// Legacy UI bootstrap rewritten as an ES Module.
// Preserves layout and behavior of the original index.html script.
// External dependencies: Leaflet ESM (imported here), public OSRM API.
// Local dependency: services/api for persistence and optional remote.

import { api } from '../services/api.js';

// Signal main module is present to avoid fallback wiring in index.html
try { window.__APP_MAIN_READY = true; } catch { }

// Leaflet is loaded via UMD <script>. It may be blocked on some networks; defer map init and guard uses.

// Expose minimal functions needed by inline HTML attributes
// We'll attach to window late after declarations

// DOM refs
const mapEl = document.getElementById('map');
const placeholder = document.getElementById('placeholder');
const tripInput = document.getElementById('tripInput');
const itineraryPanel = document.getElementById('itineraryDrawer');
const drawerOverlay = document.getElementById('drawerOverlay');
const panelBtn = document.getElementById('panelBtn');
const itineraryList = document.getElementById('itineraryList');
const equipmentBtn = document.getElementById('equipmentBtn');
const companionsBtn = document.getElementById('companionsBtn');
const companionsPage = document.getElementById('companionsPage');
const companionsList = document.getElementById('companionsList');
const groupsList = document.getElementById('groupsList');
const searchCompanionBtn = document.getElementById('searchCompanionBtn');
const addCompanionBtn = document.getElementById('addCompanionBtn');
const createGroupBtn = document.getElementById('createGroupBtn');
const closeCompanionsBtn = document.getElementById('closeCompanionsBtn');
const chatPage = document.getElementById('chatPage');
const chatTitle = document.getElementById('chatTitle');
const chatMessages = document.getElementById('chatMessages');
const chatInput = document.getElementById('chatInput');
const sendChatBtn = document.getElementById('sendChatBtn');
const closeChatBtn = document.getElementById('closeChatBtn');
const moreChatBtn = document.getElementById('moreChatBtn') || document.getElementById('chatPlusBtn');
const chatShareOverlay = document.getElementById('chatShareOverlay');
const shareItineraryBtn = document.getElementById('shareItineraryBtn');
const shareCancelBtn = document.getElementById('shareCancelBtn');
const chatShareItinOverlay = document.getElementById('chatShareItinOverlay');
const shareItinList = document.getElementById('shareItinList');
const shareItinCancelBtn = document.getElementById('shareItinCancelBtn');
const profileBtn = document.getElementById('profileBtn');
const profilePage = document.getElementById('profilePage');
const closeProfileBtn = document.getElementById('closeProfileBtn');
const profileContent = document.getElementById('profileContent');
const profileTitle = document.getElementById('profileTitle');
const routeInfo = document.getElementById('routeInfo');
const planningSpinner = document.getElementById('planningSpinner');
const complianceFeedbackLink = document.getElementById('complianceFeedbackLink');
const closeItineraryBtn = document.getElementById('closeItineraryBtn');
let planningInProgress = false;

// Stable client id for differentiating local-sent messages in legacy data
const clientId = (() => { try { let id = localStorage.getItem('clientIdV1'); if (!id) { id = 'c-' + Math.random().toString(36).slice(2) + Date.now().toString(36); localStorage.setItem('clientIdV1', id); } return id; } catch (_) { return 'c-' + Date.now(); } })();

// Custom dialog utilities - replace native alert/confirm/prompt for better UX
function showAlert(message) {
  return new Promise((resolve) => {
    const overlay = document.createElement('div');
    overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;';
    const dialog = document.createElement('div');
    dialog.style.cssText = 'background:#fff;border-radius:12px;padding:20px;max-width:400px;width:100%;box-shadow:0 4px 24px rgba(0,0,0,0.2);';
    dialog.innerHTML = `
      <div style="font-size:15px;line-height:1.5;color:#333;margin-bottom:16px;word-wrap:break-word;">${message}</div>
      <button style="width:100%;padding:10px;background:#007bff;color:#fff;border:none;border-radius:6px;font-size:14px;cursor:pointer;">确定</button>
    `;
    overlay.appendChild(dialog);
    document.body.appendChild(overlay);
    const btn = dialog.querySelector('button');
    const close = () => { overlay.remove(); resolve(); };
    btn.onclick = close;
    overlay.onclick = (e) => { if (e.target === overlay) close(); };
  });
}

function showConfirm(message) {
  return new Promise((resolve) => {
    const overlay = document.createElement('div');
    overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;';
    const dialog = document.createElement('div');
    dialog.style.cssText = 'background:#fff;border-radius:12px;padding:20px;max-width:400px;width:100%;box-shadow:0 4px 24px rgba(0,0,0,0.2);';
    dialog.innerHTML = `
      <div style="font-size:15px;line-height:1.5;color:#333;margin-bottom:16px;word-wrap:break-word;">${message}</div>
      <div style="display:flex;gap:10px;">
        <button data-action="cancel" style="flex:1;padding:10px;background:#e5e7eb;color:#374151;border:none;border-radius:6px;font-size:14px;cursor:pointer;">取消</button>
        <button data-action="confirm" style="flex:1;padding:10px;background:#007bff;color:#fff;border:none;border-radius:6px;font-size:14px;cursor:pointer;">确定</button>
      </div>
    `;
    overlay.appendChild(dialog);
    document.body.appendChild(overlay);
    const close = (result) => { overlay.remove(); resolve(result); };
    dialog.querySelector('[data-action="confirm"]').onclick = () => close(true);
    dialog.querySelector('[data-action="cancel"]').onclick = () => close(false);
    overlay.onclick = (e) => { if (e.target === overlay) close(false); };
  });
}

function showPrompt(message, defaultValue = '') {
  return new Promise((resolve) => {
    const overlay = document.createElement('div');
    overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;';
    const dialog = document.createElement('div');
    dialog.style.cssText = 'background:#fff;border-radius:12px;padding:20px;max-width:400px;width:100%;box-shadow:0 4px 24px rgba(0,0,0,0.2);';
    dialog.innerHTML = `
      <div style="font-size:15px;line-height:1.5;color:#333;margin-bottom:12px;word-wrap:break-word;">${message}</div>
      <input type="text" value="${defaultValue}" style="width:100%;padding:10px;border:1px solid #ddd;border-radius:6px;font-size:14px;margin-bottom:16px;box-sizing:border-box;" />
      <div style="display:flex;gap:10px;">
        <button data-action="cancel" style="flex:1;padding:10px;background:#e5e7eb;color:#374151;border:none;border-radius:6px;font-size:14px;cursor:pointer;">取消</button>
        <button data-action="confirm" style="flex:1;padding:10px;background:#007bff;color:#fff;border:none;border-radius:6px;font-size:14px;cursor:pointer;">确定</button>
      </div>
    `;
    overlay.appendChild(dialog);
    document.body.appendChild(overlay);
    const input = dialog.querySelector('input');
    input.focus();
    input.select();
    const close = (result) => { overlay.remove(); resolve(result); };
    const confirmBtn = dialog.querySelector('[data-action="confirm"]');
    const cancelBtn = dialog.querySelector('[data-action="cancel"]');
    confirmBtn.onclick = () => close(input.value);
    cancelBtn.onclick = () => close(null);
    overlay.onclick = (e) => { if (e.target === overlay) close(null); };
    input.onkeydown = (e) => { if (e.key === 'Enter') close(input.value); if (e.key === 'Escape') close(null); };
  });
}

// View-state persistence so refresh returns to last visible page
const LAST_VIEW_KEY = 'aiTrip:lastView.v1';
let lastOpenedChat = null; // { mode: 'direct'|'group', id: <companionId|groupId> }
function isOpen(el) { try { return !!(el && el.classList && el.classList.contains('open')); } catch { return false; } }
function saveLastView(state) { try { localStorage.setItem(LAST_VIEW_KEY, JSON.stringify({ ...state, ts: Date.now() })); } catch { } }
function readLastView() { try { const s = localStorage.getItem(LAST_VIEW_KEY); return s ? JSON.parse(s) : null; } catch { return null; } }
function markView(view, extra = {}) { saveLastView({ view, ...extra }); }
function computeAndPersistActiveView() {
  try {
    if (isOpen(chatPage) && lastOpenedChat) { saveLastView({ view: 'chat', ...lastOpenedChat }); return; }
    if (isOpen(companionsPage)) { markView('companions'); return; }
    if (isOpen(profilePage)) { markView('profile'); return; }
    if (isOpen(itineraryPanel)) { markView('overview'); return; }
    markView('home');
  } catch { }
}
function restoreLastView() {
  try {
    const st = readLastView();
    if (!st || !st.view || st.view === 'home') return;
    if (st.view === 'overview') { openPanel(); return; }
    if (st.view === 'companions') { openCompanions(); return; }
    if (st.view === 'profile') { openProfile(); return; }
    if (st.view === 'chat') {
      const mode = st.mode || 'direct';
      const id = st.id;
      if (mode === 'group') {
        const ok = (groups || []).some(g => String(g.id) === String(id));
        if (ok) { openGroupChat(id); return; }
        // fall back to companions if chat target missing
        openCompanions(); return;
      } else {
        const ok = (companions || []).some(c => String(c.id) === String(id));
        if (ok) { openChat(id); return; }
        openCompanions(); return;
      }
    }
  } catch { }
}

// History API integration for mobile back button
let appHistoryDepth = 0;
let suppressPopClose = false;
function pushAppState(view, payload) { try { history.pushState({ appView: view, payload: payload || null }, '', undefined); appHistoryDepth++; } catch { } }
function ensureBaseHistoryState() { try { history.replaceState({ appBase: true }, '', undefined); } catch { } }
window.addEventListener('popstate', (e) => {
  try {
    if (e && e.state && e.state.appView) { appHistoryDepth = Math.max(0, appHistoryDepth - 1); }
    if (suppressPopClose) { suppressPopClose = false; return; }
    // Close the currently open top-most page, emulating the page's close button
    if (isOpen(chatPage)) { closeChat(); return; }
    if (isOpen(companionsPage)) { closeCompanions(); return; }
    if (isOpen(profilePage)) { closeProfile(); return; }
    if (isOpen(itineraryPanel)) { closePanel(); return; }
  } catch { }
});

// Mobile spacing: ensure vertical gap between buttons: "旅行装备" (equipmentBtn), "同路人" (companionsBtn), and "我" (profileBtn)
// equals half of button's height on small screens.
function adjustMobileButtonsSpacing() {
  try {
    if (!companionsBtn || !profileBtn) return;
    const isMobile = window.innerWidth <= 600;
    const pbRect = profileBtn.getBoundingClientRect();
    const cbRect = companionsBtn.getBoundingClientRect();
    if (!pbRect || !cbRect) return;
    const base = 20; // baseline from bottom for profile button
    const ratio = isMobile ? (1 / 3) : 0.5; // mobile: 1/3, desktop: 1/2
    const pH = Math.max(36, Math.round(pbRect.height));
    const cH = Math.max(36, Math.round(cbRect.height));
    profileBtn.style.bottom = base + 'px';
    companionsBtn.style.bottom = Math.round(base + pH + cH * ratio) + 'px';
    
    // Position equipment button above companions button
    if (equipmentBtn) {
      const ebRect = equipmentBtn.getBoundingClientRect();
      if (ebRect) {
        const eH = Math.max(36, Math.round(ebRect.height));
        const companionsBottom = Math.round(base + pH + cH * ratio);
        equipmentBtn.style.bottom = Math.round(companionsBottom + cH + eH * ratio) + 'px';
      }
    }
  } catch { }
}

// State stores
const POI_DB = {
  '杭州': { lat: 30.274085, lng: 120.15507 },
  '黄山': { lat: 30.132, lng: 118.166 },
  '黄山风景区': { lat: 30.132, lng: 118.166 },
  '宏村': { lat: 29.853, lng: 117.997 },
  '桐庐': { lat: 29.793, lng: 119.693 },
  '徽州菜馆': { lat: 30.13, lng: 118.15 },
  // Common Hangzhou attractions
  '西溪湿地': { lat: 30.273, lng: 120.051 },
  '灵隐寺': { lat: 30.242, lng: 120.097 },
  '宋城': { lat: 30.210, lng: 120.095 },
  '西湖': { lat: 30.259, lng: 120.153 }
};

// Map provider state (lazy init). Prefer AMap when available; fallback to Leaflet.
let map = null; // truthy when any provider is ready
let mapProvider = 'auto'; // 'amap' | 'leaflet' | 'auto'
let markerLayer = null; // Leaflet-only
let routeLayer = null;  // Leaflet-only
let __didLocateOnce = false;

// Allow forcing provider via URL: ?map=amap|leaflet
const __mapPref = (() => {
  try { const v = (new URLSearchParams(location.search).get('map') || '').toLowerCase(); return v; } catch { return ''; }
})();
const __forceAmap = ['amap', 'gaode', '高德', '高德地图'].includes(__mapPref);

// AMap context
const AMAP_KEY = '1276e82158171550ad98f4ef9567d8d8'; // Public Key
const AMAP_SERVICE_HOST = '/_AMapService'; // reverse proxy path; server appends securityJsCode
let amapMap = null;
let amapMarkers = [];
let amapDrivingSegments = [];
let amapLocateCircle = null;

function setAmapSecurityConfig() {
  try {
    // Must be set BEFORE SDK script loads
    if (!window._AMapSecurityConfig) window._AMapSecurityConfig = {};
    window._AMapSecurityConfig.serviceHost = AMAP_SERVICE_HOST; // do NOT expose securityJsCode here
  } catch { }
}

let _amapLoadPromise = null;
function loadAmapSdk() {
  if (window.AMap && window.AMap.Map) return Promise.resolve(true);
  if (_amapLoadPromise) return _amapLoadPromise;
  _amapLoadPromise = new Promise((resolve) => {
    try {
      setAmapSecurityConfig();
      const s = document.createElement('script');
      s.src = `https://webapi.amap.com/maps?v=2.0&key=${encodeURIComponent(AMAP_KEY)}`;
      s.async = true;
      s.onload = () => resolve(true);
      s.onerror = () => resolve(false);
      document.head.appendChild(s);
    } catch {
      resolve(false);
    }
  });
  return _amapLoadPromise;
}
async function ipLocateFallback() {
  try {
    const withTimeout = (p, ms = 2500) => Promise.race([
      p,
      new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))
    ]);
    const tryIpApi = async () => {
      const resp = await withTimeout(fetch('https://ipapi.co/json/'));
      if (!resp || !resp.ok) throw new Error('ipapi.co failed');
      const data = await resp.json();
      const lat = Number(data.latitude || data.lat);
      const lng = Number(data.longitude || data.lon || data.lng);
      if (Number.isFinite(lat) && Number.isFinite(lng)) return { lat, lng };
      throw new Error('ipapi malformed');
    };
    const tryIpwho = async () => {
      const resp = await withTimeout(fetch('https://ipwho.is/'));
      if (!resp || !resp.ok) throw new Error('ipwho.is failed');
      const data = await resp.json();
      if (data && data.success && Number.isFinite(data.latitude) && Number.isFinite(data.longitude)) {
        return { lat: data.latitude, lng: data.longitude };
      }
      throw new Error('ipwho malformed');
    };
    let pos = null;
    try { pos = await tryIpApi(); } catch { }
    if (!pos) { try { pos = await tryIpwho(); } catch { } }
    if (!pos) return false;
    if (map) {
      map.setView([pos.lat, pos.lng], 11);
      try { if (markerLayer) window.L && window.L.circle && window.L.circle([pos.lat, pos.lng], { radius: 2000, color: '#2b8cff', fillOpacity: 0.08 }).addTo(markerLayer); } catch { }
      __didLocateOnce = true;
      try { showMiniToast('已定位到大致位置'); } catch { }
    }
    return true;
  } catch (e) {
    return false;
  }
}
async function initMapIfNeeded() {
  try {
    if (map || !mapEl) return;
    // Try AMap first
    let useAmap = false;
    try {
      const waitMs = __forceAmap ? 6000 : 3000;
      const ok = await Promise.race([
        loadAmapSdk(),
        new Promise((res) => setTimeout(() => res(false), waitMs))
      ]);
      useAmap = !!(ok && window.AMap && window.AMap.Map);
    } catch { useAmap = false; }
    if (useAmap) { mapProvider = 'amap'; initAmap(); return; }
    // If explicitly requested AMap, try a second-chance wait before giving up
    if (__forceAmap) {
      try {
        // One more attempt without race; avoid blocking too long by capping at ~4s
        await new Promise((resolve) => {
          let done = false;
          loadAmapSdk().then(() => { if (!done) { done = true; resolve(true); } });
          setTimeout(() => { if (!done) { done = true; resolve(false); } }, 4000);
        });
      } catch { }
      if (window.AMap && window.AMap.Map) { mapProvider = 'amap'; initAmap(); return; }
    }
    // Fallback to Leaflet
    if (!window.L) { showMapServiceNoticeToast(); return; }
    const L = window.L;
    mapProvider = 'leaflet';
    map = L.map(mapEl, { zoomControl: true }).fitWorld();
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors' }).addTo(map);
    markerLayer = L.layerGroup().addTo(map);
    routeLayer = L.geoJSON(null, { style: { color: '#2b8cff', weight: 5, opacity: 0.9 } }).addTo(map);
    const canGeo = !!(window.isSecureContext && navigator && 'geolocation' in navigator);
    if (canGeo) {
      map.locate({ setView: true, maxZoom: 11 });
      setTimeout(() => { try { if (!__didLocateOnce) ipLocateFallback(); } catch { } }, 2500);
    } else {
      ipLocateFallback();
    }
    map.on('locationerror', async () => {
      // console.log('定位失败，尝试使用大致位置。');
      try { showMiniToast('定位失败，已尝试使用大致位置'); } catch { }
      await ipLocateFallback();
    });
    map.on('locationfound', (e) => { __didLocateOnce = true; L.circle(e.latlng, { radius: 80, color: '#2b8cff', fillOpacity: 0.12 }).addTo(markerLayer); });
    map.on('click', () => { closePanel(); });

    // Late upgrade: if AMap SDK loads shortly after and AMap was preferred, switch provider
    if (__forceAmap) {
      setTimeout(() => {
        try {
          if (mapProvider === 'leaflet' && window.AMap && window.AMap.Map) {
            try { map && map.remove && map.remove(); } catch { }
            map = null; markerLayer = null; routeLayer = null;
            mapProvider = 'amap';
            initAmap();
          }
        } catch { }
      }, 2500);
    }
  } catch (e) {
    console.warn('initMapIfNeeded failed', e);
    showMapServiceNoticeToast();
  }
}

function initAmap() {
  try {
    const AMap = window.AMap;
    amapMap = new AMap.Map(mapEl, { zoom: 11, center: [120.15507, 30.274085], viewMode: '2D' });
    map = amapMap;
    AMap.plugin(['AMap.ToolBar', 'AMap.Scale'], () => {
      // Zoom +/- toolbar at left-top
      const tb = new AMap.ToolBar({ position: 'LT', offset: new AMap.Pixel(10, 10) });
      amapMap.addControl(tb);
    });
    AMap.plugin('AMap.Geolocation', () => {
      const geo = new AMap.Geolocation({ enableHighAccuracy: true, timeout: 5000 });
      geo.getCurrentPosition((status, result) => {
        try { __didLocateOnce = (status === 'complete') || (status === 'SUCCESS'); } catch { }
        if ((status === 'complete' || status === 'SUCCESS') && result && result.position) {
          const pos = result.position;
          amapMap.setCenter(pos);
          try {
            if (amapLocateCircle) amapMap.remove(amapLocateCircle);
            amapLocateCircle = new AMap.CircleMarker({ center: pos, radius: 6, strokeColor: '#2b8cff', strokeWeight: 2, fillColor: '#2b8cff', fillOpacity: 0.25, zIndex: 120 });
            amapMap.add(amapLocateCircle);
          } catch { }
        }
      });
    });
    amapMap.on('click', () => { try { closePanel(); } catch { } });
  } catch (e) {
    console.warn('initAmap failed', e);
    showMapServiceNoticeToast();
  }
}

function getMapCenterLatLng() {
  try {
    if (mapProvider === 'amap' && amapMap) { const c = amapMap.getCenter(); return [c.getLat(), c.getLng()]; }
    if (mapProvider === 'leaflet' && map && map.getCenter) { const c = map.getCenter(); return [c.lat, c.lng]; }
  } catch { }
  return [30.274085, 120.15507];
}

function nudgeLeafletControls() {
  const ctrl = document.querySelector('.leaflet-control-zoom');
  if (ctrl) { ctrl.style.top = '10px'; ctrl.style.left = '10px'; ctrl.style.zIndex = '650'; }
}

// Render empty state ASAP (before images/scripts finish loading)
document.addEventListener('DOMContentLoaded', () => {
  try { renderItineraryOverview(); } catch { }
});

window.addEventListener('load', async () => {
  setTimeout(nudgeLeafletControls, 300);
  initMapIfNeeded();
  ensureBaseHistoryState();
  // Load app data in parallel to speed up first render; itinerary empty-state already shown
  const starters = [
    loadUsers(),
    loadCompanions(),
    loadGroups(),
    loadChats(),
    loadItineraries()
  ];
  await Promise.allSettled(starters);
  renderGroups();
  // After layout, align mobile left-side buttons spacing
  setTimeout(adjustMobileButtonsSpacing, 0);
  window.addEventListener('resize', adjustMobileButtonsSpacing);
  // Restore last view (companions/chat/profile) after data loaded
  restoreLastView();
  try { window.__APP_MAIN_READY = true; } catch { }
});

// External links
function isMobile() {
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
function buildAmapNavUrl(fromName, fromCoords, toName, toCoords) {
  // Goal: 在高德打开后自动填充起终点（网页端 & 移动端）。
  // 优先使用官方 uri.amap.com 导航格式（支持坐标+名称），若有坐标：
  //   https://uri.amap.com/navigation?from=lng,lat,name&to=lng,lat,name&mode=car&policy=0&src=ai_trip_planner&coordinate=gaode&callnative=0
  // 移动端同时保留原生 scheme 以便直接拉起 App（如已安装）。
  try {
    const hasCoords = Array.isArray(fromCoords) && fromCoords.length === 2 && Array.isArray(toCoords) && toCoords.length === 2;
    const sName = (fromName || '起点').trim();
    const dName = (toName || '终点').trim();
    if (hasCoords) {
      // our stored order is [lat, lng]; Amap expects lng,lat
      const [sLat, sLng] = fromCoords; const [dLat, dLng] = toCoords;
      const encSName = encodeURIComponent(sName);
      const encDName = encodeURIComponent(dName);
      const webUrl = `https://uri.amap.com/navigation?from=${encodeURIComponent(sLng + "," + sLat + "," + sName)}&to=${encodeURIComponent(dLng + "," + dLat + "," + dName)}&mode=car&policy=0&src=ai_trip_planner&coordinate=gaode&callnative=0`;
      if (isMobile()) {
        // 优先尝试 App 深链；若浏览器/环境不支持会自然留在当前页，可长按复制或手动回退再用分享菜单打开 Web 链接
        // 需要更稳妥回落可改造成点击监听 + setTimeout 打开 webUrl，这里按“优先App”需求直接返回 scheme。
        const scheme = `amapuri://route/plan/?slat=${sLat}&slon=${sLng}&sname=${encSName}&dlat=${dLat}&dlon=${dLng}&dname=${encDName}&dev=0&t=0`;
        return scheme;
      }
      return webUrl;
    }
    // 无坐标时退回名称搜索/驾驶页面；名称参数会填入输入框
    const f = encodeURIComponent(sName);
    const t = encodeURIComponent(dName);
    return `https://www.amap.com/dir?from=${f}&to=${t}&mode=car`;
  } catch (_e) {
    // 最终兜底：只带终点搜索
    return `https://www.amap.com/search?query=${encodeURIComponent(toName || '目的地')}`;
  }
}
function buildDianpingUrl(keyword) { return `https://www.dianping.com/search/keyword/1/0_${encodeURIComponent(keyword)}`; }
function buildCtripUrl(keyword) { return `https://you.ctrip.com/globalsearch/?keyword=${encodeURIComponent(keyword)}`; }

function openNavTo(lat, lng, name) {
  try {
    const center = getMapCenterLatLng();
    const from = [center[0], center[1]];
    const url = buildAmapNavUrl('当前位置', from, name || '目的地', [lat, lng]);
    try { showNavDisclaimerOnce(); } catch { }
    window.open(url, '_blank', 'noopener');
  } catch (e) {
    const url = `https://www.amap.com/search?query=${encodeURIComponent(name || '目的地')}`;
    window.open(url, '_blank', 'noopener');
  }
}

// 一次性导航提示（起始位置偏差）
function showNavDisclaimerOnce() {
  const id = 'navDisclaimerToast';
  try { const old = document.getElementById(id); if (old) old.remove(); } catch { }
  const div = document.createElement('div');
  div.id = id;
  div.textContent = '起始位置可能存在偏差，请认真核对。';
  div.setAttribute('role', 'status');
  div.style.cssText = 'position:fixed;left:50%;bottom:54px;transform:translateX(-50%);max-width:82%;background:rgba(0,0,0,0.78);color:#fff;padding:10px 14px;font-size:12px;line-height:1.4;border-radius:8px;z-index:1601;box-shadow:0 4px 16px rgba(0,0,0,0.3);opacity:0;transition:opacity .25s';
  document.body.appendChild(div);
  requestAnimationFrame(() => { div.style.opacity = '1'; });
  setTimeout(() => { div.style.opacity = '0'; setTimeout(() => { try { div.remove(); } catch { } }, 320); }, 4200);
}

// Panel controls
function openPanel() { itineraryPanel.classList.add('open'); drawerOverlay.style.display = 'block'; itineraryPanel.setAttribute('aria-hidden', 'false'); panelBtn.setAttribute('aria-expanded', 'true'); computeAndPersistActiveView(); pushAppState('overview'); }
function closePanel() { itineraryPanel.classList.remove('open'); drawerOverlay.style.display = 'none'; itineraryPanel.setAttribute('aria-hidden', 'true'); panelBtn.setAttribute('aria-expanded', 'false'); computeAndPersistActiveView(); }
function togglePanel() { itineraryPanel.classList.contains('open') ? closePanel() : openPanel(); }
// Open-only to avoid double-binding conflicts with fallback wiring
if (panelBtn) panelBtn.addEventListener('click', openPanel);
if (drawerOverlay) drawerOverlay.addEventListener('click', closePanel);
if (closeItineraryBtn) closeItineraryBtn.addEventListener('click', closePanel);

function expandInput() { placeholder.style.display = 'none'; tripInput.style.display = 'block'; tripInput.focus(); }
window.expandInput = expandInput;

// Trip input: Enter to submit; Shift+Enter for newline; handle IME composition
if (tripInput) {
  let composing = false;
  tripInput.addEventListener('compositionstart', () => composing = true);
  tripInput.addEventListener('compositionend', () => composing = false);
  tripInput.addEventListener('keydown', async (e) => {
    if (e.key === 'Enter') {
      if (e.shiftKey) { return; }
      if (composing) { return; }
      if (planningInProgress || tripInput.disabled) { e.preventDefault(); return; }
      e.preventDefault();
      await planTripWithAI();
    }
  });
}

// Companions and groups
let companions = [];
let groups = [];
let companionInvites = [];
let companionInvitePollTimer = null;
let lastCompanionInvitesSnapshot = [];
let lastCompanionsSnapshot = [];
let recentlyDeletedCompanionUserIds = {}; // userId -> timestamp
async function persistCompanions() { const ok = await api.setCompanions(companions); if (!ok) { showMiniToast('保存同路人失败：后台不可用'); } }
async function loadCompanions() {
  try { companions = await api.getCompanions(); } catch { companions = []; }
  try { lastCompanionsSnapshot = companions.slice(); } catch { }
  renderCompanions();
}
async function loadCompanionInvites() { try { companionInvites = await api.listCompanionInvites(); } catch { companionInvites = []; } }
async function persistGroups() { const ok = await api.setGroups(groups); if (!ok) { showMiniToast('保存群聊失败：后台不可用'); } }
async function loadGroups() {
  try {
    groups = await api.getGroups();
    // Normalize legacy groups to userId-based membership
    let changed = false;
    groups = (groups || []).map(g => {
      const uids = extractMemberUserIdsFromGroup(g);
      if (!Array.isArray(g.memberUserIds) || String(g.memberUserIds.map(String)) !== String(uids)) {
        changed = true;
        return { ...g, memberUserIds: uids, memberIds: uids };
      }
      return g;
    });
    if (changed) { await persistGroups(); }
  } catch { groups = []; }
}

async function refreshCompanionsData() {
  await Promise.all([
    loadCompanions(),
    loadCompanionInvites(),
    (async () => { try { users = await api.getUsers(); } catch { } })()
  ]);
  renderCompanions();
}

function updateCompanionsBadge() {
  try {
    if (!companionsBtn) return;
    const myId = currentUserId;
    const pending = (companionInvites || []).filter(i => i.status === 'pending' && (String(i.toUserId) === String(myId))).length;
    let badge = companionsBtn.querySelector('.badge');
    if (pending > 0) {
      if (!badge) {
        badge = document.createElement('span');
        badge.className = 'badge';
        badge.style.cssText = 'position:absolute;top:-4px;right:-4px;background:#ef4444;color:#fff;font-size:10px;min-width:16px;height:16px;padding:0 4px;border-radius:16px;display:flex;align-items:center;justify-content:center;font-weight:600;box-shadow:0 0 0 2px #fff;';
        // Do NOT override original positioning (likely fixed/absolute) to avoid layout shifts
        // companionsBtn.style.position = 'relative'; // removed intentionally
        companionsBtn.appendChild(badge);
      }
      badge.textContent = pending > 99 ? '99+' : String(pending);
    } else if (badge) {
      badge.remove();
    }
    // Ensure any old inline position override is cleared (from previous versions)
    try { if (companionsBtn.style && companionsBtn.style.position) { companionsBtn.style.position = ''; } } catch { }
    // Recalc spacing after badge changes
    try { adjustMobileButtonsSpacing(); } catch { }
  } catch { }
}

async function pollCompanionInvitesOnce() {
  try {
    const prev = Array.isArray(lastCompanionInvitesSnapshot) ? lastCompanionInvitesSnapshot : [];
    const fresh = await api.listCompanionInvites();
    if (!Array.isArray(fresh)) return;
    companionInvites = fresh;
    // Detect new incoming pending invites
    const myId = currentUserId;
    const prevIds = new Set(prev.map(i => String(i.id)));
    fresh.filter(i => i.status === 'pending' && String(i.toUserId) === String(myId) && !prevIds.has(String(i.id)))
      .forEach(inv => { showMiniToast('收到同路人邀请'); });
    // Detect accepted/declined results for invites I sent
    const prevById = {}; prev.forEach(i => prevById[String(i.id)] = i);
    let acceptedNow = false;
    fresh.forEach(i => {
      const old = prevById[String(i.id)];
      if (old && old.status === 'pending' && i.status !== 'pending' && String(i.fromUserId) === String(myId)) {
        if (i.status === 'accepted') { showMiniToast('对方已接受邀请'); acceptedNow = true; }
        else if (i.status === 'declined') showMiniToast('邀请被拒绝');
      }
    });
    lastCompanionInvitesSnapshot = fresh.slice();
    if (acceptedNow) {
      try { await loadCompanions(); } catch { }
    }
    // Remote deletion detection: fetch companions (lightweight; already done in loadCompanions on acceptance)
    if (!acceptedNow) {
      try {
        const latest = await api.getCompanions();
        if (Array.isArray(latest)) {
          if (lastCompanionsSnapshot.length) {
            const myId2 = currentUserId;
            const prevOwned = lastCompanionsSnapshot.filter(x => String(x.ownerUserId) === String(myId2) && !x.isSelf);
            const nowOwnedMap = new Set(latest.filter(x => String(x.ownerUserId) === String(myId2)).map(x => String(x.userId)));
            prevOwned.forEach(p => {
              const uidStr = String(p.userId);
              if (!nowOwnedMap.has(uidStr)) {
                const ts = recentlyDeletedCompanionUserIds[uidStr];
                const suppressed = ts && (Date.now() - ts < 15000);
                if (!suppressed) { showMiniToast('对方已解除同路人关系'); }
              }
            });
          }
          companions = latest.slice();
          lastCompanionsSnapshot = latest.slice();
        }
      } catch { }
    }
    if (companionsPage && companionsPage.classList.contains('open')) { renderCompanions(); }
    updateCompanionsBadge();
  } catch { }
}

function startCompanionInvitePolling() {
  if (companionInvitePollTimer) return;
  pollCompanionInvitesOnce();
  companionInvitePollTimer = setInterval(pollCompanionInvitesOnce, 10000); // 10s interval
}
function stopCompanionInvitePolling() { if (companionInvitePollTimer) { clearInterval(companionInvitePollTimer); companionInvitePollTimer = null; } }

function generateDefaultGroupName(memberUserIds) {
  try {
    const MAX_LEN = 18; // 基础主体最大长度（不含日期后缀）
    const names = (memberUserIds || []).map(uid => {
      const u = (users || []).find(x => String(x.id) === String(uid));
      return u ? (u.nickname || u.phone || ('用户' + uid)) : ('用户' + uid);
    });
    // 自己优先放在第一个位置
    const myId = currentUserId;
    names.sort((a, b) => {
      // 若 a 是“我”对应昵称/手机号则靠前
      const am = a.includes('我') || a.includes('I');
      const bm = b.includes('我') || b.includes('I');
      if (am && !bm) return -1; if (bm && !am) return 1; return 0;
    });
    let joined = names.slice(0, 5).join('、'); // 最多取前5个
    const now = new Date();
    const y = now.getFullYear();
    const m = String(now.getMonth() + 1).padStart(2, '0');
    const d = String(now.getDate()).padStart(2, '0');
    const dateSuffix = `-${y}${m}${d}`;
    if (joined.length > MAX_LEN) {
      joined = joined.slice(0, MAX_LEN - 1) + '…';
    }
    return (joined || '群聊') + dateSuffix;
  } catch { return '群聊'; }
}

function extractMemberUserIdsFromGroup(g) {
  try {
    if (Array.isArray(g.memberUserIds) && g.memberUserIds.length) return g.memberUserIds.map(x => String(x));
    if (Array.isArray(g.memberIds)) {
      return g.memberIds.map(x => {
        const s = String(x);
        if (/^self-/.test(s)) return s.slice(5);
        if (/^comp-/.test(s)) { const parts = s.split('-'); return parts[2] || null; }
        return s; // assume userId
      }).filter(Boolean);
    }
  } catch { }
  return [];
}

function ensureSelfCompanion() {
  try {
    const me = users.find(u => u.id === currentUserId);
    if (!me) return;
    const selfId = `self-${me.id}`;
    let found = false;
    companions = (companions || []).map(c => {
      if (c && (c.id === selfId || (c.isSelf && String(c.userId) === String(me.id)))) {
        found = true;
        return { ...c, id: selfId, name: me.nickname || me.phone || '我', contact: me.phone || '', isSelf: true, userId: me.id };
      }
      return c;
    });
    if (!found) {
      const selfEntry = { id: selfId, name: me.nickname || me.phone || '我', contact: me.phone || '', createdAt: Date.now(), isSelf: true, userId: me.id };
      companions = [selfEntry, ...companions];
    }
    persistCompanions();
  } catch { }
}

function renderCompanions() {
  companionsList.innerHTML = '';
  ensureSelfCompanion();
  const myId = currentUserId;
  // Pending invites UI
  const incoming = (companionInvites || []).filter(i => String(i.toUserId) === String(myId) && i.status === 'pending');
  const outgoing = (companionInvites || []).filter(i => String(i.fromUserId) === String(myId) && i.status === 'pending');
  if (incoming.length || outgoing.length) {
    const box = document.createElement('div');
    box.style.cssText = 'border:1px solid #ddd;border-radius:8px;padding:8px 10px;margin-bottom:10px;background:#fafafa;font-size:12px;line-height:1.5';
    let html = '';
    if (incoming.length) {
      html += '<div style="font-weight:600;margin-bottom:4px">待我确认</div>';
      incoming.forEach(inv => {
        const fromUser = (users || []).find(u => String(u.id) === String(inv.fromUserId));
        const name = fromUser ? (fromUser.nickname || fromUser.phone || ('用户' + inv.fromUserId)) : ('用户' + inv.fromUserId);
        html += `<div style='display:flex;align-items:center;gap:8px;margin:4px 0'><span>${name}</span><button data-accept='${inv.id}' class='companions-add-btn' style='padding:2px 8px;font-size:11px'>接受</button><button data-decline='${inv.id}' class='companions-close-btn' style='padding:2px 8px;font-size:11px'>拒绝</button></div>`;
      });
    }
    if (outgoing.length) {
      html += '<div style="font-weight:600;margin:8px 0 4px">已发送（等待对方）</div>';
      outgoing.forEach(inv => {
        const toUser = (users || []).find(u => String(u.id) === String(inv.toUserId));
        const name = toUser ? (toUser.nickname || toUser.phone || ('用户' + inv.toUserId)) : ('用户' + inv.toUserId);
        html += `<div style='display:flex;align-items:center;gap:8px;margin:4px 0'><span>${name}</span><span style='color:#888;font-size:11px'>待接受</span></div>`;
      });
    }
    box.innerHTML = html || '';
    box.addEventListener('click', async (e) => {
      const accBtn = e.target.closest && e.target.closest('button[data-accept]');
      const decBtn = e.target.closest && e.target.closest('button[data-decline]');
      if (accBtn) {
        const id = accBtn.getAttribute('data-accept');
        try { await api.acceptCompanionInvite(id); } catch { }
        await refreshCompanionsData();
      } else if (decBtn) {
        const id = decBtn.getAttribute('data-decline');
        try { await api.declineCompanionInvite(id); } catch { }
        await refreshCompanionsData();
      }
    });
    companionsList.appendChild(box);
  }
  if (!companions.length) {
    const empty = document.createElement('div');
    empty.style.color = '#888'; empty.style.marginTop = '8px';
    empty.textContent = '还没有同路人，点右上角＋添加';
    companionsList.appendChild(empty);
    return;
  }
  const visible = companions.filter(c => !!c && ((c.isSelf && (String(c.userId) === String(myId) || String(c.id) === `self-${myId}`)) || String(c.ownerUserId || '') === String(myId)));
  visible.forEach(c => {
    const item = document.createElement('div'); item.className = 'companions-item';
    const isSelf = !!(c && c.isSelf && (String(c.userId) === String(myId) || String(c.id) === `self-${myId}`));
    const left = document.createElement('div');
    let liveName = c.name;
    let liveContact = c.contact;
    try {
      const u = (users || []).find(u => String(u.id) === String(c.userId));
      if (u) { liveName = u.nickname || u.phone || liveName; liveContact = u.phone || liveContact; }
    } catch { }
    if (isSelf) {
      left.innerHTML = `<b>${(liveName || '')}<span style="margin-left:6px;font-size:11px;color:#155eef;border:1px solid #bfdbfe;background:#eff6ff;border-radius:10px;padding:1px 6px;vertical-align:middle">本人</span></b><div style="color:#888;font-size:12px">${liveContact || ''}</div>`;
    } else {
      left.innerHTML = `<b>${(liveName || '')}</b><div style="color:#888;font-size:12px">${liveContact || ''}</div>`;
    }
    item.appendChild(left);
    if (isSelf) {
      // Allow messaging to self
      item.style.cursor = 'pointer';
      item.addEventListener('click', () => { openChat(c.id); });
    } else {
      const del = document.createElement('button');
      del.textContent = '删除';
      del.onclick = async () => {
        const ok = await showConfirm('确认删除该同路人？删除后将无法继续与TA直接聊天。');
        if (!ok) return;
        try {
          const targetUserId = c.userId;
          recentlyDeletedCompanionUserIds[String(targetUserId)] = Date.now();
          // remove both directions
          companions = companions.filter(x => !(
            (String(x.ownerUserId) === String(currentUserId) && String(x.userId) === String(targetUserId)) ||
            (String(x.ownerUserId) === String(targetUserId) && String(x.userId) === String(currentUserId))
          ));
          await persistCompanions();
          try { lastCompanionsSnapshot = companions.slice(); } catch { }
          renderCompanions();
          showMiniToast('已删除该同路人');
        } catch { }
      };
      item.appendChild(del);
      item.addEventListener('click', (e) => { if (e.target === del) return; openChat(c.id); });
    }
    companionsList.appendChild(item);
  });
  renderGroups();
}
function renderGroups() {
  groupsList.innerHTML = '';
  const myId = currentUserId;
  const visibleGroups = (groups || []).filter(g => extractMemberUserIdsFromGroup(g).some(uid => String(uid) === String(myId)));
  if (!visibleGroups.length) { const empty = document.createElement('div'); empty.style.color = '#888'; empty.textContent = '暂无群聊，点击“建群”创建'; groupsList.appendChild(empty); return; }
  visibleGroups.forEach(g => {
    const item = document.createElement('div'); item.className = 'companions-item';
    const uids = extractMemberUserIdsFromGroup(g);
    const ownerUid = g && g.ownerUserId != null ? String(g.ownerUserId) : (uids && uids.length ? String(uids[0]) : null);
    const namesText = uids.map(uid => {
      const u = (users || []).find(x => String(x.id) === String(uid));
      if (!u) return `用户${uid}`;
      const isMe = String(uid) === String(myId);
      const isOwner = ownerUid && String(uid) === String(ownerUid);
      const name = u.nickname || u.phone || `用户${uid}`;
      let label = isMe ? `${name}（我）` : name;
      if (isOwner) { label += '（群主）'; }
      if (isMe && isOwner) { label = `${name}（我、群主）`; }
      return label;
    }).join('、');
    const left = document.createElement('div'); left.innerHTML = `<b>${g.name}</b><div style=\"color:#888;font-size:12px\">成员：${namesText || '-'}</div>`;
    const isOwner = String(g.ownerUserId || '') === String(myId);
    const actionBtn = document.createElement('button');
    if (isOwner) {
      // Owners: show two actions 解散 and 退出
      actionBtn.textContent = '解散';
      actionBtn.onclick = async () => {
        const ok = await showConfirm('确认解散该群聊？解散后所有成员将无法继续在该群聊天。');
        if (!ok) return;
        groups = groups.filter(x => x.id !== g.id);
        await persistGroups();
        renderGroups();
        try { showMiniToast && showMiniToast('已解散该群聊'); } catch { }
        if (currentChatMode === 'group' && String(currentChatId) === String(g.id)) {
          try { closeChat(); } catch { }
        }
      };
      const leaveBtn = document.createElement('button');
      leaveBtn.textContent = '退出';
      leaveBtn.onclick = () => { openTransferOwnerOverlay(g.id); };
      const addBtn = document.createElement('button');
      addBtn.textContent = '加人';
      addBtn.onclick = () => { openAddMembersOverlay(g.id); };
      // Right-side container to prevent space-between from splitting buttons
      const rightControls = document.createElement('div');
      rightControls.style.display = 'flex';
      rightControls.style.alignItems = 'center';
      rightControls.style.gap = '8px';
      // Order left-to-right: 加人, 退出, 解散 (so right-to-left remains 解散, 退出, 加人)
      rightControls.appendChild(addBtn);
      rightControls.appendChild(leaveBtn);
      rightControls.appendChild(actionBtn);
      item.appendChild(left);
      item.appendChild(rightControls);
      item.addEventListener('click', (e) => { if (e.target === actionBtn || e.target === leaveBtn) return; openGroupChat(g.id); });
      groupsList.appendChild(item);
      return;
    } else {
      actionBtn.textContent = '退出';
      actionBtn.onclick = async () => {
        const ok = await showConfirm('确认退出该群聊？退出后将不再接收该群消息。');
        if (!ok) return;
        try {
          // Remove myself from memberUserIds/memberIds
          const uid = String(myId);
          g.memberUserIds = (g.memberUserIds || []).filter(x => String(x) !== uid);
          g.memberIds = (g.memberIds || []).filter(x => String(x) !== uid && String(x) !== `self-${uid}`);
          // If group becomes empty or only one left, optionally clean it up
          const remaining = extractMemberUserIdsFromGroup(g);
          if (!remaining.length) {
            groups = groups.filter(x => x.id !== g.id);
          }
          await persistGroups();
          renderGroups();
          try { showMiniToast && showMiniToast('已退出该群聊'); } catch { }
          // If currently viewing this group chat, close it
          if (currentChatMode === 'group' && String(currentChatId) === String(g.id)) {
            try { closeChat(); } catch { }
          }
        } catch { }
      };
      const addBtn = document.createElement('button');
      addBtn.textContent = '加人';
      addBtn.style.marginRight = '8px';
      addBtn.onclick = () => { openAddMembersOverlay(g.id); };
      const rightControls = document.createElement('div');
      rightControls.style.display = 'flex';
      rightControls.style.alignItems = 'center';
      rightControls.style.gap = '8px';
      // Order left-to-right: 加人, 退出
      rightControls.appendChild(addBtn);
      rightControls.appendChild(actionBtn);
      item.appendChild(left);
      item.appendChild(rightControls);
      item.addEventListener('click', (e) => { if (e.target === actionBtn || e.target === addBtn) return; openGroupChat(g.id); });
      groupsList.appendChild(item);
      return;
    }
    item.appendChild(left); item.appendChild(actionBtn);
    item.addEventListener('click', (e) => { if (e.target === actionBtn) return; openGroupChat(g.id); });
    groupsList.appendChild(item);
  });
}

async function openCompanions() {
  const user = users.find(u => u.id === currentUserId);
  if (!user) {
    postLoginRedirect = 'companions';
    if (profilePage) { profilePage.classList.add('open'); profilePage.setAttribute('aria-hidden', 'false'); if (profileBtn) profileBtn.setAttribute('aria-expanded', 'true'); renderLoginView(); pushAppState('profile'); }
    return;
  }
  // Load invites first so pending items appear
  try { await loadCompanionInvites(); } catch { }
  try { await loadCompanions(); } catch { }
  // loadCompanions calls renderCompanions internally, but re-render to ensure invite box present
  try { renderCompanions(); } catch { }
  startCompanionInvitePolling();
  companionsPage.classList.add('open'); companionsPage.setAttribute('aria-hidden', 'false'); companionsBtn.setAttribute('aria-expanded', 'true');
  try { adjustMobileButtonsSpacing(); } catch { }
  markView('companions');
  pushAppState('companions');
}
function closeCompanions() { companionsPage.classList.remove('open'); companionsPage.setAttribute('aria-hidden', 'true'); companionsBtn.setAttribute('aria-expanded', 'false'); computeAndPersistActiveView(); }
// Recalculate spacing after overlay close to avoid stale measurements
try {
  const _origCloseComp = closeCompanions;
  closeCompanions = function () { _origCloseComp(); try { adjustMobileButtonsSpacing(); } catch { } };
} catch { }
async function addCompanion() {
  const phone = await showPrompt('输入同路人账号（手机号）以发送邀请');
  if (!phone) return;
  const trimmedPhone = phone.trim();
  if (!trimmedPhone) return;
  if (!validPhone(trimmedPhone)) { await showAlert('手机号格式不正确'); return; }
  let resp = null;
  try { resp = await api.createCompanionInvite({ phone: trimmedPhone }); } catch { }
  if (!resp) { await showAlert('邀请发送失败（服务不可用或本地模式不支持）'); return; }
  if (resp.duplicate) { await showAlert('存在待处理或已发出的相同邀请'); }
  else { showMiniToast('邀请已发送，等待对方接受'); }
  await refreshCompanionsData();
}
if (equipmentBtn) equipmentBtn.addEventListener('click', () => showAlert('功能开发中，敬请期待'));
if (companionsBtn) companionsBtn.addEventListener('click', openCompanions);
if (searchCompanionBtn) searchCompanionBtn.addEventListener('click', () => showAlert('功能开发中，敬请期待'));
if (addCompanionBtn) addCompanionBtn.addEventListener('click', addCompanion);
if (closeCompanionsBtn) closeCompanionsBtn.addEventListener('click', closeCompanions);

// Create group overlay (constructed from original HTML behavior)
const createGroupOverlay = document.createElement('section');
createGroupOverlay.className = 'sheet-overlay'; createGroupOverlay.id = 'createGroupOverlay';
createGroupOverlay.innerHTML = `
  <div class="sheet" role="dialog" aria-label="创建群聊">
    <div class="sheet-title">创建群聊</div>
    <div style="margin-bottom:8px">
      <input id="groupNameInput" placeholder="输入群名称" style="width:100%;border:1px solid #ddd;border-radius:8px;padding:8px 10px" />
    </div>
    <div id="groupPickList" class="sheet-list" style="max-height:40vh;overflow:auto"></div>
    <div style="display:flex;gap:8px;margin-top:8px">
      <button id="createGroupConfirmBtn" class="companions-add-btn" style="flex:1">创建</button>
      <button id="createGroupCancelBtn" class="companions-close-btn" style="flex:1">取消</button>
    </div>
  </div>`;
document.body.appendChild(createGroupOverlay);

// Transfer owner overlay for owner exit
const transferOwnerOverlay = document.createElement('section');
transferOwnerOverlay.className = 'sheet-overlay';
transferOwnerOverlay.id = 'transferOwnerOverlay';
transferOwnerOverlay.innerHTML = `
  <div class="sheet" role="dialog" aria-label="转移群主后退出">
    <div class="sheet-title">退出前选择新群主</div>
    <div id="transferOwnerPickList" class="sheet-list" style="max-height:40vh;overflow:auto"></div>
    <div style="display:flex;gap:8px;margin-top:8px">
      <button id="transferOwnerConfirmBtn" class="companions-add-btn" style="flex:1">确认</button>
      <button id="transferOwnerCancelBtn" class="companions-close-btn" style="flex:1">取消</button>
    </div>
  </div>`;
document.body.appendChild(transferOwnerOverlay);
let transferOwnerTargetGroupId = null;
async function openTransferOwnerOverlay(groupId) {
  const g = (groups || []).find(x => String(x.id) === String(groupId));
  if (!g) return;
  const myId = currentUserId;
  const candidates = extractMemberUserIdsFromGroup(g).filter(uid => String(uid) !== String(myId));
  if (!candidates.length) { await showAlert('群里没有其他成员，无法退出。请先邀请成员或选择解散群聊。'); return; }
  const pick = transferOwnerOverlay.querySelector('#transferOwnerPickList');
  pick.innerHTML = '';
  candidates.forEach(uid => {
    let label = `用户${uid}`;
    try { const u = (users || []).find(x => String(x.id) === String(uid)); if (u) { label = u.nickname || u.phone || label; } } catch { }
    const row = document.createElement('label');
    row.className = 'sheet-action';
    row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.gap = '8px';
    row.innerHTML = `<input type="radio" name="transferOwnerPick" data-uid="${uid}"> <span>${label}</span>`;
    pick.appendChild(row);
  });
  transferOwnerTargetGroupId = groupId;
  transferOwnerOverlay.classList.add('open');
  transferOwnerOverlay.setAttribute('aria-hidden', 'false');
}
function closeTransferOwnerOverlay() { transferOwnerOverlay.classList.remove('open'); transferOwnerOverlay.setAttribute('aria-hidden', 'true'); transferOwnerTargetGroupId = null; }
transferOwnerOverlay.addEventListener('click', (e) => { if (e.target === transferOwnerOverlay) closeTransferOwnerOverlay(); });
const transferOwnerConfirmBtn = transferOwnerOverlay.querySelector('#transferOwnerConfirmBtn');
const transferOwnerCancelBtn = transferOwnerOverlay.querySelector('#transferOwnerCancelBtn');
transferOwnerCancelBtn.addEventListener('click', closeTransferOwnerOverlay);
transferOwnerConfirmBtn.addEventListener('click', async () => {
  const myId = currentUserId;
  const groupId = transferOwnerTargetGroupId;
  if (!groupId) return closeTransferOwnerOverlay();
  const g = (groups || []).find(x => String(x.id) === String(groupId));
  if (!g) return closeTransferOwnerOverlay();
  const sel = transferOwnerOverlay.querySelector('input[type="radio"][name="transferOwnerPick"]:checked');
  if (!sel) { await showAlert('请选择一位新群主'); return; }
  const nextOwner = sel.getAttribute('data-uid');
  if (!nextOwner) { await showAlert('请选择一位新群主'); return; }
  try {
    g.ownerUserId = nextOwner;
    // remove myself
    const uid = String(myId);
    g.memberUserIds = (g.memberUserIds || []).filter(x => String(x) !== uid);
    g.memberIds = (g.memberIds || []).filter(x => String(x) !== uid && String(x) !== `self-${uid}`);
    await persistGroups();
    renderGroups();
    try { showMiniToast && showMiniToast('已退出该群聊'); } catch { }
    // Close chat if currently viewing this group
    if (currentChatMode === 'group' && String(currentChatId) === String(g.id)) {
      try { closeChat(); } catch { }
    }
  } catch { }
  closeTransferOwnerOverlay();
});

// Add members overlay
const addMembersOverlay = document.createElement('section');
addMembersOverlay.className = 'sheet-overlay';
addMembersOverlay.id = 'addMembersOverlay';
addMembersOverlay.innerHTML = `
  <div class="sheet" role="dialog" aria-label="添加群成员">
    <div class="sheet-title">添加群成员</div>
    <div id="addMembersPickList" class="sheet-list" style="max-height:40vh;overflow:auto"></div>
    <div style="display:flex;gap:8px;margin-top:8px">
      <button id="addMembersConfirmBtn" class="companions-add-btn" style="flex:1">添加</button>
      <button id="addMembersCancelBtn" class="companions-close-btn" style="flex:1">取消</button>
    </div>
  </div>`;
document.body.appendChild(addMembersOverlay);
let addMembersTargetGroupId = null;
function openAddMembersOverlay(groupId) {
  const g = (groups || []).find(x => String(x.id) === String(groupId));
  if (!g) return;
  const myId = currentUserId;
  const memberSet = new Set(extractMemberUserIdsFromGroup(g).map(String));
  const visible = (companions || []).filter(c => !!c && ((c.isSelf && (String(c.userId) === String(myId) || String(c.id) === `self-${myId}`)) || String(c.ownerUserId || '') === String(myId)));
  const candidates = visible.filter(c => !memberSet.has(String(c.userId)));
  const pick = addMembersOverlay.querySelector('#addMembersPickList');
  pick.innerHTML = '';
  if (!candidates.length) {
    const empty = document.createElement('div');
    empty.style.color = '#888';
    empty.textContent = '没有可添加的同路人';
    pick.appendChild(empty);
  } else {
    candidates.forEach(c => {
      const row = document.createElement('label');
      row.className = 'sheet-action';
      row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.gap = '8px';
      row.innerHTML = `<input type="checkbox" data-uid="${c.userId}"> <span>${c.name}${c.contact ? `（${c.contact}）` : ''}</span>`;
      pick.appendChild(row);
    });
  }
  addMembersTargetGroupId = groupId;
  addMembersOverlay.classList.add('open');
  addMembersOverlay.setAttribute('aria-hidden', 'false');
}
function closeAddMembersOverlay() { addMembersOverlay.classList.remove('open'); addMembersOverlay.setAttribute('aria-hidden', 'true'); addMembersTargetGroupId = null; }
addMembersOverlay.addEventListener('click', (e) => { if (e.target === addMembersOverlay) closeAddMembersOverlay(); });
const addMembersConfirmBtn = addMembersOverlay.querySelector('#addMembersConfirmBtn');
const addMembersCancelBtn = addMembersOverlay.querySelector('#addMembersCancelBtn');
addMembersCancelBtn.addEventListener('click', closeAddMembersOverlay);
addMembersConfirmBtn.addEventListener('click', async () => {
  const groupId = addMembersTargetGroupId;
  if (!groupId) return closeAddMembersOverlay();
  const g = (groups || []).find(x => String(x.id) === String(groupId));
  if (!g) return closeAddMembersOverlay();
  const checks = Array.from(addMembersOverlay.querySelectorAll('input[type="checkbox"][data-uid]'));
  const selected = checks.filter(ch => ch.checked).map(ch => ch.getAttribute('data-uid')).filter(Boolean);
  if (!selected.length) { await showAlert('请选择至少一位同路人'); return; }
  try {
    const memberSet = new Set(extractMemberUserIdsFromGroup(g).map(String));
    selected.forEach(uid => memberSet.add(String(uid)));
    g.memberUserIds = Array.from(memberSet);
    g.memberIds = g.memberUserIds.slice();
    await persistGroups();
    renderGroups();
    try { showMiniToast && showMiniToast('已添加到群聊'); } catch { }
  } catch { }
  closeAddMembersOverlay();
});

function openCreateGroup() {
  const pick = createGroupOverlay.querySelector('#groupPickList');
  pick.innerHTML = '';
  const myId = currentUserId;
  const visible = (companions || []).filter(c => !!c && ((c.isSelf && (String(c.userId) === String(myId) || String(c.id) === `self-${myId}`)) || String(c.ownerUserId || '') === String(myId)));
  visible.forEach(c => {
    const row = document.createElement('label');
    row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.gap = '8px';
    row.className = 'sheet-action';
    row.innerHTML = `<input type="checkbox" data-uid="${c.userId}"> <span>${c.name}${c.contact ? `（${c.contact}）` : ''}</span>`;
    pick.appendChild(row);
  });
  createGroupOverlay.classList.add('open');
  createGroupOverlay.setAttribute('aria-hidden', 'false');
  const nameInput = createGroupOverlay.querySelector('#groupNameInput'); if (nameInput) { nameInput.value = ''; nameInput.focus(); }
}
function closeCreateGroup() { createGroupOverlay.classList.remove('open'); createGroupOverlay.setAttribute('aria-hidden', 'true'); }
if (createGroupBtn) createGroupBtn.addEventListener('click', openCreateGroup);
createGroupOverlay.addEventListener('click', (e) => { if (e.target === createGroupOverlay) closeCreateGroup(); });
const createGroupConfirmBtn = createGroupOverlay.querySelector('#createGroupConfirmBtn');
const createGroupCancelBtn = createGroupOverlay.querySelector('#createGroupCancelBtn');
createGroupConfirmBtn.addEventListener('click', async () => {
  const myId = currentUserId;
  const visible = (companions || []).filter(c => !!c && ((c.isSelf && (String(c.userId) === String(myId) || String(c.id) === `self-${myId}`)) || String(c.ownerUserId || '') === String(myId)));
  const visUidSet = new Set(visible.map(c => String(c.userId)));
  const checks = Array.from(createGroupOverlay.querySelectorAll('input[type="checkbox"][data-uid]'));
  let memberUserIds = checks.filter(ch => ch.checked).map(ch => ch.getAttribute('data-uid')).filter(uid => uid && visUidSet.has(String(uid)));
  // always include myself
  if (!memberUserIds.some(uid => String(uid) === String(myId))) memberUserIds = [String(myId), ...memberUserIds];
  // 至少需要我 + 1 位其他成员
  const others = memberUserIds.filter(uid => String(uid) !== String(myId));
  if (others.length === 0) { await showAlert('请选择至少一位同路人'); return; }
  const nameInput = createGroupOverlay.querySelector('#groupNameInput');
  let name = (nameInput && nameInput.value || '').trim();
  if (!name) { name = generateDefaultGroupName(memberUserIds); }
  const g = { id: Date.now(), name, memberUserIds: memberUserIds.slice(), memberIds: memberUserIds.slice(), ownerUserId: myId, createdAt: Date.now() };
  groups.push(g);
  await persistGroups();
  renderGroups();
  closeCreateGroup();
  openGroupChat(g.id);
});
createGroupCancelBtn.addEventListener('click', closeCreateGroup);

// Chat state
let chatSessions = {};
let groupChats = {};
let currentChatId = null;
let currentChatMode = 'direct';
let chatPollTimer = null;
let chatInlineEditingItinId = null;
let chatInlineEditingTemp = null;
let chatItineraryOverrides = {};
let chatRemoteItineraries = {};
let chatItineraryCollapse = {};

function persistChatCollapse() { api.setChatItineraryCollapse(chatItineraryCollapse); }

async function loadChats() {
  const [d, g, ri, col] = await Promise.all([
    api.getChats(),
    api.getGroupChats(),
    api.getChatRemoteItineraries(),
    api.getChatItineraryCollapse(),
  ]);
  chatSessions = d || {};
  groupChats = g || {};
  chatRemoteItineraries = ri || {};
  chatItineraryCollapse = col || {};
  migrateDirectChatKeysIfNeeded();
}

function migrateDirectChatKeysIfNeeded() {
  // Migrate legacy direct chat keys to canonical dm:<a>-<b>
  try {
    const migrated = {};
    let changed = false;
    const reComp = /^comp-(\d+)-(\d+)$/;
    const reSelf = /^self-(\d+)$/;
    Object.keys(chatSessions || {}).forEach(k => {
      let nk = k;
      let m;
      if ((m = reComp.exec(k))) {
        const a = m[1], b = m[2];
        const [x, y] = [a, b].sort((p, q) => (p === q ? 0 : (p < q ? -1 : 1)));
        nk = `dm:${x}-${y}`;
      } else if ((m = reSelf.exec(k))) {
        const a = m[1]; nk = `dm:${a}-${a}`;
      }
      if (nk !== k) changed = true;
      const arr = chatSessions[k] || [];
      migrated[nk] = (migrated[nk] || []).concat(Array.isArray(arr) ? arr : []);
    });
    if (changed) { chatSessions = migrated; persistChats(); }
  } catch { }
}

async function pollChatsOnce() {
  try {
    const [newDirect, newGroup] = await Promise.all([api.getChats(), api.getGroupChats()]);
    const directStrBefore = JSON.stringify(chatSessions || {});
    const groupStrBefore = JSON.stringify(groupChats || {});
    if (newDirect) chatSessions = newDirect;
    if (newGroup) groupChats = newGroup;
    migrateDirectChatKeysIfNeeded();
    const changed = (directStrBefore !== JSON.stringify(chatSessions || {})) || (groupStrBefore !== JSON.stringify(groupChats || {}));
    if (changed && chatPage && chatPage.classList.contains('open')) {
      renderChat();
    }
  } catch { }
}

function startChatPolling() { if (chatPollTimer) return; chatPollTimer = setInterval(pollChatsOnce, 1000); }
function stopChatPolling() { if (chatPollTimer) { clearInterval(chatPollTimer); chatPollTimer = null; } }
async function persistChats() {
  const ok1 = await api.setChats(chatSessions);
  const ok2 = await api.setGroupChats(groupChats);
  if (!ok1 || !ok2) { showMiniToast('保存聊天失败：后台不可用'); }
}
async function persistRemoteItins() {
  const ok = await api.setChatRemoteItineraries(chatRemoteItineraries);
  if (!ok) { showMiniToast('保存共享行程失败：后台不可用'); }
}

function renderChat(opts = {}) {
  chatMessages.innerHTML = '';
  // Ensure vertical stack and allow per-message alignment
  try {
    chatMessages.style.display = 'flex';
    chatMessages.style.flexDirection = 'column';
    chatMessages.style.gap = '8px';
    chatMessages.style.boxSizing = 'border-box';
    chatMessages.style.overflowX = 'hidden';
  } catch { }
  const msgs = currentChatMode === 'group' ? (groupChats[currentChatId] || []) : (chatSessions[currentChatId] || []);
  msgs.forEach(m => {
    const div = document.createElement('div');
    let isMe = false;
    try {
      if (m && m.fromUserId != null) {
        isMe = String(m.fromUserId) === String(currentUserId);
      } else if (m && m.clientId) {
        isMe = String(m.clientId) === String(clientId);
      } else if (m && typeof m.from === 'string') {
        // Legacy messages without author metadata: force to left by default
        if (m.from === 'them') { isMe = false; }
        else if (m.from === 'me') { isMe = false; }
      }
    } catch { }
    div.className = 'msg ' + (isMe ? 'me' : 'them');
    // Align: others on left, myself on right
    div.style.alignSelf = isMe ? 'flex-end' : 'flex-start';
    div.style.float = 'none';
    if (isMe) { div.style.marginLeft = 'auto'; div.style.marginRight = '0'; } else { div.style.marginLeft = '0'; div.style.marginRight = 'auto'; }
    div.style.maxWidth = '85%';
    div.style.wordBreak = 'break-word';
    div.style.lineHeight = '1.35';
    div.style.padding = '6px 10px';
    div.style.borderRadius = '10px';
    div.style.background = isMe ? '#e8f0fe' : '#f5f5f5';
    div.style.boxSizing = 'border-box';
    if (m.type === 'share-itinerary' && m.payload && m.payload.itinId) {
      const baseItin = itineraries.find(it => String(it.id) === String(m.payload.itinId));
      const override = chatItineraryOverrides[m.payload.itinId];
      // Prefer embedded snapshot to ensure consistent view across devices
      const snapshot = m.payload.snapshot || null;
      const itin = override
        ? { id: baseItin ? baseItin.id : m.payload.itinId, name: override.name || (snapshot && snapshot.name) || (baseItin && baseItin.name), days: override.days }
        : (snapshot ? { id: m.payload.itinId, name: snapshot.name, days: snapshot.days } : baseItin);
      div.dataset.itinId = m.payload.itinId;
      try { if (m.payload && m.payload.snapshot) { div.dataset.snapshot = btoa(unescape(encodeURIComponent(JSON.stringify(m.payload.snapshot)))); } } catch { }
      const name = itin ? (itin.name || '未命名行程') : '(已删除的行程)';
      let fullDaysHtml = '';
      if (itin && Array.isArray(itin.days) && itin.days.length) {
        const collapseState = (chatItineraryCollapse[itin.id] && chatItineraryCollapse[itin.id].days) || {};
        itin.days.forEach((d, di) => {
          const hasRecord = Object.prototype.hasOwnProperty.call(collapseState, di);
          const isCollapsed = hasRecord ? !!collapseState[di] : true;
          let evs = '';
          if (!isCollapsed) {
            (d.events || []).forEach(ev => {
              let typeLabel = '事件';
              if (ev.type === 'drive') typeLabel = '驾驶';
              else if (ev.type === 'activity') typeLabel = ev.subtype === 'food' ? '美食' : '游玩';
              else if (ev.type === 'accommodation') typeLabel = '住宿';
              const title = (ev.title || ev.text || '未命名').replace(/</g, '&lt;');
              const notes = (ev.notes || '').replace(/</g, '&lt;');
              evs += `<div style='border:1px solid #eee;border-radius:6px;padding:6px 8px;margin-bottom:6px;background:#fafafa'>
                <div style='display:flex;gap:6px;align-items:flex-start;margin-bottom:2px;font-size:12px'>
                  <span style='font-size:10px;color:#fff;background:#888;padding:2px 5px;border-radius:4px;line-height:1'>${typeLabel}</span>
                  <div style='flex:1'>${title}</div>
                </div>
                ${notes ? `<div style='font-size:11px;color:#555;white-space:pre-wrap'>${notes}</div>` : ''}
              </div>`;
            });
          }
          const innerToggle = "<div data-chat-day-toggle='" + di + "' style='user-select:none;cursor:pointer;display:flex;align-items:center;gap:6px;font-weight:600;font-size:12px;margin-bottom:" + (isCollapsed ? 0 : 4) + "px'>"
            + "<span style='display:inline-block;transform:rotate(" + (isCollapsed ? '-90deg' : '0deg') + ");transition:transform .2s;font-size:10px'>▶</span>"
            + "第" + (di + 1) + "天"
            + "<span style='font-size:10px;color:#666;font-weight:400'>" + ((d.events || []).length) + "件</span>"
            + "</div>";
          const innerBody = isCollapsed ? '' : (evs || "<div style='font-size:11px;color:#999'>无事件</div>");
          fullDaysHtml += "<div style='border:1px solid #ddd;border-radius:6px;padding:6px 8px;margin-bottom:8px'>"
            + innerToggle + innerBody + "</div>";
        });
      } else {
        fullDaysHtml = `<div style='font-size:12px;color:#888;margin:4px 0 6px'>暂无天</div>`;
      }
      const hasOverride = !!chatItineraryOverrides[m.payload.itinId];
      div.innerHTML = `<div style=\"font-weight:600;margin-bottom:6px;display:flex;align-items:center;gap:6px\"><span>${name}</span>${hasOverride ? '<span style=\"font-size:10px;color:#d35400;border:1px solid #d35400;padding:1px 4px;border-radius:4px\">未更新</span>' : ''}</div>` +
        `<div>${fullDaysHtml}</div>` +
        (() => {
          const isMine = String(m.fromUserId) === String(currentUserId); const alreadySaved = itineraries.some(it => String(it.id) === String(m.payload.itinId)); const saveBtnHtml = (!isMine && !alreadySaved) ? '<button class=\"mini-btn save-shared-itin\" style=\"font-size:11px;padding:2px 8px\">保存到我的行程</button>' : ''; return `<div style=\"display:flex;gap:6px;flex-wrap:wrap;margin-top:4px\">\
          <button class=\"mini-btn show-shared-itin\" style=\"font-size:11px;padding:2px 8px\">地图显示</button>${saveBtnHtml}\
          ${hasOverride ? '<button class=\"mini-btn revert-shared-itin\" style=\"font-size:11px;padding:2px 8px\" data-chat-itin-revert-outside=\"1\">恢复</button>' : ''}
        </div>`;
        })();
    } else {
      div.textContent = m.text;
    }
    chatMessages.appendChild(div);
  });
  // Scroll unless caller suppressed (e.g., toggling collapse)
  if (!opts.noAutoScroll) { scrollChatToBottom(); }
}

function scrollChatToBottom() {
  try {
    if (!chatPage || !chatPage.classList.contains('open')) return;
    // Scroll the actual scroll container (chatPage)
    chatPage.scrollTop = chatPage.scrollHeight;
    // Double-pass to handle late layout / image loads / font reflow
    requestAnimationFrame(() => { try { chatPage.scrollTop = chatPage.scrollHeight; } catch { } });
    setTimeout(() => { try { chatPage.scrollTop = chatPage.scrollHeight; } catch { } }, 60);
  } catch { }
}

function dmThreadIdForUsers(a, b) { const [x, y] = [String(a), String(b)].sort((p, q) => (p === q ? 0 : (p < q ? -1 : 1))); return `dm:${x}-${y}`; }
function openChat(companionId) { const c = companions.find(x => x.id === companionId); if (!c) return; const threadId = dmThreadIdForUsers(currentUserId, c.userId || currentUserId); currentChatId = threadId; currentChatMode = 'direct'; lastOpenedChat = { mode: 'direct', id: companionId }; markView('chat', lastOpenedChat); chatTitle.textContent = c.name; chatPage.classList.add('open'); chatPage.setAttribute('aria-hidden', 'false'); renderChat(); chatInput.focus(); startChatPolling(); pushAppState('chat', lastOpenedChat); }
function openGroupChat(groupId) { const g = groups.find(x => x.id === groupId); if (!g) return; currentChatId = groupId; currentChatMode = 'group'; lastOpenedChat = { mode: 'group', id: groupId }; markView('chat', lastOpenedChat); chatTitle.textContent = g.name; chatPage.classList.add('open'); chatPage.setAttribute('aria-hidden', 'false'); renderChat(); chatInput.focus(); startChatPolling(); pushAppState('chat', lastOpenedChat); }
function closeChat() { chatPage.classList.remove('open'); chatPage.setAttribute('aria-hidden', 'true'); currentChatId = null; currentChatMode = 'direct'; stopChatPolling(); computeAndPersistActiveView(); }

async function sendChat() {
  const text = (chatInput.value || '').trim(); if (!text || currentChatId == null) return;
  const arr = currentChatMode === 'group' ? (groupChats[currentChatId] = groupChats[currentChatId] || []) : (chatSessions[currentChatId] = chatSessions[currentChatId] || []);
  arr.push({ from: 'me', fromUserId: currentUserId, clientId, text, ts: Date.now() }); chatInput.value = ''; await persistChats(); renderChat();
}
if (sendChatBtn) sendChatBtn.addEventListener('click', sendChat);
if (closeChatBtn) closeChatBtn.addEventListener('click', closeChat);
if (chatInput) chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { sendChat(); } });

// Delegated handler: toggle collapse/expand for shared itinerary days
chatMessages.addEventListener('click', (e) => {
  const toggleEl = e.target.closest && e.target.closest('[data-chat-day-toggle]');
  if (!toggleEl) return;
  const dayIndex = Number(toggleEl.getAttribute('data-chat-day-toggle'));
  const wrapMsg = toggleEl.closest('.msg');
  if (!wrapMsg || !wrapMsg.dataset.itinId) return;
  const itinId = wrapMsg.dataset.itinId;
  chatItineraryCollapse[itinId] = chatItineraryCollapse[itinId] || { days: {} };
  const daysState = chatItineraryCollapse[itinId].days;
  const prev = Object.prototype.hasOwnProperty.call(daysState, dayIndex) ? !!daysState[dayIndex] : true; // default collapsed true
  daysState[dayIndex] = !prev; // toggle
  persistChatCollapse();
  // Re-render chat without forcing scroll to bottom so user keeps position
  renderChat({ noAutoScroll: true });
});

// Delegated handler: show itinerary on map from shared itinerary message
chatMessages.addEventListener('click', async (e) => {
  const showBtn = e.target.closest && e.target.closest('.show-shared-itin');
  if (!showBtn) return;
  const wrapMsg = showBtn.closest('.msg');
  if (!wrapMsg || !wrapMsg.dataset.itinId) return;
  const itinId = wrapMsg.dataset.itinId;
  const msgObj = (currentChatMode === 'group' ? (groupChats[currentChatId] || []) : (chatSessions[currentChatId] || [])).find(m => m.type === 'share-itinerary' && m.payload && String(m.payload.itinId) === String(itinId));
  const baseItin = itineraries.find(it => String(it.id) === String(itinId));
  const override = chatItineraryOverrides[itinId];
  let snapshot = msgObj && msgObj.payload && msgObj.payload.snapshot;
  if (!snapshot) {
    // Try dataset snapshot embedded in DOM
    try { const raw = wrapMsg.dataset.snapshot; if (raw) { snapshot = JSON.parse(decodeURIComponent(escape(atob(raw)))); } } catch { }
  }
  // Prefer snapshot over local base itinerary to avoid mismatched content across devices
  const finalItin = override
    ? { id: itinId, name: (override.name || (snapshot && snapshot.name) || (baseItin && baseItin.name) || '行程'), days: override.days }
    : (snapshot ? { id: itinId, name: snapshot.name || '行程', days: snapshot.days } : baseItin);
  if (!finalItin) { await showAlert('该行程已被删除'); return; }
  try { initMapIfNeeded(); updateMapFromItinerary(finalItin.days); } catch { }
  // If no visible route (routeInfo hidden) give a gentle notice
  try { if (routeInfo && routeInfo.style.display === 'none') { /* maybe no drive segments */ /* console.log('共享行程无可绘制路线'); */ } } catch { }
  // Always show map disclaimer for shared itinerary
  try { showMapDisclaimerOnce(); } catch { }
  // Close chat to reveal map (mobile) then open itinerary drawer for context
  try { closeChat(); } catch { }
  // Ensure companions overlay & itinerary drawer both closed for pure map view
  try { closeCompanions(); } catch { }
  try { closePanel(); } catch { }
});

// Delegated handler: save shared itinerary into my itineraries
chatMessages.addEventListener('click', async (e) => {
  const saveBtn = e.target.closest && e.target.closest('.save-shared-itin');
  if (!saveBtn) return;
  const wrapMsg = saveBtn.closest('.msg');
  if (!wrapMsg || !wrapMsg.dataset.itinId) return;
  const itinId = wrapMsg.dataset.itinId;
  if (itineraries.some(it => String(it.id) === String(itinId))) { saveBtn.disabled = true; saveBtn.textContent = '已保存'; return; }
  // Reconstruct itinerary via same fallback chain used for map display
  const msgObj = (currentChatMode === 'group' ? (groupChats[currentChatId] || []) : (chatSessions[currentChatId] || [])).find(m => m.type === 'share-itinerary' && m.payload && String(m.payload.itinId) === String(itinId));
  const baseItin = itineraries.find(it => String(it.id) === String(itinId));
  const override = chatItineraryOverrides[itinId];
  let snapshot = msgObj && msgObj.payload && msgObj.payload.snapshot;
  if (!snapshot) { try { const raw = wrapMsg.dataset.snapshot; if (raw) { snapshot = JSON.parse(decodeURIComponent(escape(atob(raw)))); } } catch { } }
  // Prefer snapshot content over local base itinerary to ensure we save exactly what was shared
  const source = override
    ? { id: itinId, name: override.name || (snapshot && snapshot.name) || (baseItin && baseItin.name) || '行程', days: override.days }
    : (snapshot ? { id: itinId, name: snapshot.name || '行程', days: snapshot.days } : baseItin);
  if (!source) { await showAlert('无法保存：缺少行程数据'); return; }
  // Create new id to avoid clashing with original owner if they mutate theirs later
  const newId = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
  const cloned = { id: newId, name: source.name + '（来自分享）', createdAt: Date.now(), days: JSON.parse(JSON.stringify(source.days || [])) };
  itineraries.push(cloned);
  try { persistItineraries(); } catch { }
  renderItineraryOverview();
  saveBtn.disabled = true; saveBtn.textContent = '已保存';
});

// Share from chat
function openShareSheet() { chatShareOverlay.classList.add('open'); chatShareOverlay.setAttribute('aria-hidden', 'false'); }
function closeShareSheet() { chatShareOverlay.classList.remove('open'); chatShareOverlay.setAttribute('aria-hidden', 'true'); }
function openShareItinSheet() {
  shareItinList.innerHTML = '';
  if (!itineraries.length) { const empty = document.createElement('div'); empty.style.color = '#888'; empty.textContent = '暂无行程可分享'; shareItinList.appendChild(empty); }
  else { itineraries.forEach((itin, idx) => { const btn = document.createElement('button'); btn.className = 'sheet-action'; const dateStr = new Date(itin.createdAt).toLocaleString(); btn.textContent = `${itin.name || ('行程 ' + (idx + 1))}  ·  ${dateStr}`; btn.onclick = () => shareItineraryToCurrentChat(idx); shareItinList.appendChild(btn); }); }
  chatShareItinOverlay.classList.add('open'); chatShareItinOverlay.setAttribute('aria-hidden', 'false');
}
function closeShareItinSheet() { chatShareItinOverlay.classList.remove('open'); chatShareItinOverlay.setAttribute('aria-hidden', 'true'); }
async function shareItineraryToCurrentChat(itinIdx) { if (currentChatId == null) return; const itin = itineraries[itinIdx]; if (!itin) return; const linkText = `[行程分享] ${itin.name}`; const snapshot = { name: itin.name, days: itin.days }; const arr = currentChatMode === 'group' ? (groupChats[currentChatId] = groupChats[currentChatId] || []) : (chatSessions[currentChatId] = chatSessions[currentChatId] || []); arr.push({ from: 'me', fromUserId: currentUserId, clientId, text: linkText, ts: Date.now(), type: 'share-itinerary', payload: { itinId: itin.id, snapshot } }); await persistChats(); renderChat(); closeShareItinSheet(); closeShareSheet(); }
if (moreChatBtn) moreChatBtn.addEventListener('click', openShareSheet);
if (shareCancelBtn) shareCancelBtn.addEventListener('click', closeShareSheet);
if (shareItineraryBtn) shareItineraryBtn.addEventListener('click', () => { closeShareSheet(); openShareItinSheet(); });
if (shareItinCancelBtn) shareItinCancelBtn.addEventListener('click', closeShareItinSheet);

// Itineraries and map layers
// markerLayer/routeLayer are created in initMapIfNeeded()
let itineraries = [];
let itinerariesLoading = true;
let itinerariesError = null;
let selectedItineraryIndex = -1;
let isEditing = false;
let editingItineraryIndex = -1;
let editingDays = null;
let dragState = null;
let touchDrag = null;

function parseTextToRoute(text) {
  const names = Object.keys(POI_DB).filter(n => text.includes(n));
  if (names.length === 0) { const maybe = text.match(/去\s*([\u4e00-\u9fa5]{2,5})/g); if (maybe) { maybe.forEach(m => { const nm = m.replace(/去\s*/, ''); if (POI_DB[nm]) names.push(nm); }); } }
  const start = text.includes('杭州') ? '杭州' : '杭州';
  let route = [start, ...names.filter(n => n !== start)];
  if (route.length === 1) route.push('黄山', '宏村');
  if (route[route.length - 1] !== start) { route = [...route, start]; }
  return route;
}

function haversineMeters(a, b) {
  const toRad = v => v * Math.PI / 180; const R = 6371000; const dLat = toRad(b[0] - a[0]); const dLon = toRad(b[1] - a[1]); const lat1 = toRad(a[0]); const lat2 = toRad(b[0]); const sinDLat = Math.sin(dLat / 2); const sinDLon = Math.sin(dLon / 2); const aa = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon; const c = 2 * Math.atan2(Math.sqrt(aa), Math.sqrt(1 - aa)); return R * c;
}

function buildItineraryOverview(routeNames, coordsArray, poiNearby) {
  const avgSpeedKmh = 60; const maxDrivingHoursPerDay = 4;
  const legs = []; for (let i = 0; i < coordsArray.length - 1; i++) { const from = routeNames[i]; const to = routeNames[i + 1]; const a = coordsArray[i]; const b = coordsArray[i + 1]; const meters = haversineMeters(a, b); const km = meters / 1000; const hours = km / avgSpeedKmh; legs.push({ from, to, meters, km, hours, distanceStr: `${km.toFixed(1)} km`, durationStr: `${(hours).toFixed(2)} h`, arriveCoords: b }); }
  const days = []; let day = { events: [], accommodation: null }; let cumHours = 0;
  for (let i = 0; i < legs.length; i++) {
    const leg = legs[i]; if (day.events.length > 0 && (cumHours + leg.hours) > maxDrivingHoursPerDay) { days.push(day); day = { events: [], accommodation: null }; cumHours = 0; }
    day.events.push({ type: 'drive', text: `${leg.from} → ${leg.to}`, distance: leg.distanceStr, duration: leg.durationStr, from: leg.from, to: leg.to, fromCoords: coordsArray[i], toCoords: leg.arriveCoords }); cumHours += leg.hours;
    const activities = poiNearby.filter(p => haversineMeters(leg.arriveCoords, p.coords) <= 15000 && p.type !== 'hotel').slice(0, 2).map(p => ({ name: p.name, kind: p.type }));
    if (leg.to === '黄山' && !activities.find(a => a.name === '黄山')) activities.unshift({ name: '黄山', kind: 'spot' });
    activities.forEach(a => day.events.push({ type: 'activity', text: a.name, subtype: a.kind, coords: leg.arriveCoords }));
    const hotels = poiNearby.filter(p => haversineMeters(leg.arriveCoords, p.coords) <= 20000 && p.type === 'hotel');
    if (hotels.length) { day.accommodation = hotels[0].name; day.events.push({ type: 'accommodation', text: hotels[0].name, coords: leg.arriveCoords }); }
  }
  if (day.events.length || day.accommodation) days.push(day);
  const start = routeNames[0]; const lastLeg = legs[legs.length - 1];
  if (lastLeg && lastLeg.to === start) { const lastDay = days[days.length - 1]; if (lastDay) { const idx = lastDay.events.map(e => e.type).lastIndexOf('drive'); if (idx >= 1) { const moved = lastDay.events.splice(idx, 1)[0]; const newDay = { events: [moved], accommodation: null }; const originCoords = legs[legs.length - 1].arriveCoords || (legs[legs.length - 2] && legs[legs.length - 2].arriveCoords); if (originCoords) { const shortActs = poiNearby.filter(p => haversineMeters(originCoords, p.coords) <= 15000 && p.type !== 'hotel').slice(0, 2); shortActs.forEach(poi => newDay.events.push({ type: 'activity', text: poi.name, subtype: poi.type })); } days.push(newDay); } } }
  return days;
}

function renderItineraryOverview() {
  itineraryList.innerHTML = '';
  if (itinerariesLoading) { const li = document.createElement('li'); li.textContent = '加载中…'; itineraryList.appendChild(li); return; }
  if (!itineraries.length) { const li = document.createElement('li'); li.textContent = '暂无行程，输入需求后点击回车生成'; itineraryList.appendChild(li); return; }
  itineraries.forEach((itin, itinIdx) => {
    const days = (isEditing && editingItineraryIndex === itinIdx) ? editingDays : (itin.days || []);
    const wrap = document.createElement('li'); wrap.style.border = '1px solid #eee'; wrap.style.borderRadius = '8px'; wrap.style.padding = '8px'; wrap.style.marginBottom = '12px';
    const header = document.createElement('div'); header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; header.style.marginBottom = '8px';
    const title = document.createElement('div'); title.innerHTML = `<b>${itin.name || ('行程 ' + (itinIdx + 1))}</b><span style="color:#888;margin-left:8px;font-size:12px">${new Date(itin.createdAt).toLocaleString()}</span>`;
    const actions = document.createElement('div'); actions.style.display = 'flex'; actions.style.gap = '6px';
    const showBtn = document.createElement('button'); showBtn.textContent = '地图显示'; showBtn.onclick = () => { showItineraryOnMap(itinIdx); closePanel(); };
    const shareBtn = document.createElement('button'); shareBtn.textContent = '分享'; shareBtn.onclick = () => openItineraryShareSheet(itinIdx);
    const editBtn = document.createElement('button');
    const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消';
    const renameBtn = document.createElement('button'); renameBtn.textContent = '重命名';
    const delBtn = document.createElement('button'); delBtn.textContent = '删除';
    const inEditHeader = (isEditing && editingItineraryIndex === itinIdx);
    editBtn.textContent = inEditHeader ? '保存' : '编辑';
    cancelBtn.style.display = inEditHeader ? 'inline-block' : 'none';
    showBtn.style.display = inEditHeader ? 'none' : 'inline-block';
    shareBtn.style.display = inEditHeader ? 'none' : 'inline-block';
    renameBtn.style.display = inEditHeader ? 'none' : 'inline-block';
    delBtn.style.display = inEditHeader ? 'none' : 'inline-block';
    actions.appendChild(showBtn); actions.appendChild(shareBtn); actions.appendChild(editBtn); actions.appendChild(cancelBtn); actions.appendChild(renameBtn); actions.appendChild(delBtn);
    header.appendChild(title); header.appendChild(actions); wrap.appendChild(header);
    editBtn.onclick = async () => { if (isEditing && editingItineraryIndex === itinIdx) { itineraries[itinIdx].days = editingDays; isEditing = false; editingItineraryIndex = -1; editingDays = null; await api.setItineraries(itineraries); renderItineraryOverview(); await recomputeDriveMetaFor(itinIdx); showItineraryOnMap(itinIdx); if (chatPage.classList.contains('open')) renderChat(); } else { isEditing = true; editingItineraryIndex = itinIdx; editingDays = JSON.parse(JSON.stringify(itin.days || [])); renderItineraryOverview(); } };
    cancelBtn.onclick = () => { isEditing = false; editingItineraryIndex = -1; editingDays = null; renderItineraryOverview(); };
    renameBtn.onclick = async () => { const nv = await showPrompt('输入行程名称', itin.name || ''); if (nv) { itineraries[itinIdx].name = nv.trim(); api.setItineraries(itineraries); renderItineraryOverview(); if (chatPage.classList.contains('open')) renderChat(); } };
    delBtn.onclick = async () => { if (await showConfirm('确认删除该行程？')) { itineraries.splice(itinIdx, 1); if (selectedItineraryIndex === itinIdx) selectedItineraryIndex = -1; api.setItineraries(itineraries); renderItineraryOverview(); clearMap(); if (chatPage.classList.contains('open')) renderChat(); } };

    const daysList = document.createElement('ul');
    days.forEach((d, dayIdx) => {
      const dayEl = document.createElement('li');
      const dHeader = document.createElement('div'); dHeader.innerText = `第${dayIdx + 1}天：`; dHeader.style.fontWeight = '600'; dHeader.style.marginBottom = '6px'; dayEl.appendChild(dHeader);
      if (isEditing && editingItineraryIndex === itinIdx) {
        const topZone = document.createElement('div'); topZone.className = 'day-dropzone'; topZone.dataset.dayIndex = String(dayIdx); topZone.dataset.pos = 'top'; topZone.dataset.itinIndex = String(itinIdx);
        topZone.addEventListener('dragover', (e) => { if (dragState) { e.preventDefault(); topZone.classList.add('active'); } });
        topZone.addEventListener('dragleave', () => topZone.classList.remove('active'));
        topZone.addEventListener('drop', (e) => { e.preventDefault(); topZone.classList.remove('active'); if (!dragState) return; moveEvent(dragState.itinIdx, dragState.dayIdx, dragState.fromIndex, itinIdx, dayIdx, 0); dragState = null; });
        setupTouchZone(topZone, itinIdx, dayIdx, 'top'); dayEl.appendChild(topZone);
      }
      (d.events || []).forEach((ev, evIndex) => {
        const p = document.createElement('div'); p.className = 'event-row'; p.style.display = 'flex'; p.style.alignItems = 'center'; p.style.gap = '6px';
        p.dataset.dayIndex = String(dayIdx); p.dataset.evIndex = String(evIndex); p.dataset.itinIndex = String(itinIdx);
        const inEdit = (isEditing && editingItineraryIndex === itinIdx);
        if (inEdit) {
          // drag n drop + touch drag
          p.classList.add('draggable'); p.setAttribute('draggable', 'true');
          p.addEventListener('dragstart', (e) => { dragState = { itinIdx: itinIdx, dayIdx: dayIdx, fromIndex: evIndex }; e.dataTransfer.effectAllowed = 'move'; });
          p.addEventListener('dragover', (e) => { if (dragState) { e.preventDefault(); p.classList.add('drag-over'); e.dataTransfer.dropEffect = 'move'; } });
          p.addEventListener('dragleave', () => p.classList.remove('drag-over'));
          p.addEventListener('drop', (e) => { e.preventDefault(); p.classList.remove('drag-over'); if (!dragState) return; moveEvent(dragState.itinIdx, dragState.dayIdx, dragState.fromIndex, itinIdx, dayIdx, evIndex); dragState = null; });
          p.addEventListener('dragend', () => { dragState = null; });
          setupTouchRow(p, itinIdx, dayIdx, evIndex);

          // [+] insert before
          const plusBefore = document.createElement('button'); plusBefore.className = 'mini-btn'; plusBefore.innerText = '+'; plusBefore.title = '在此之前插入'; plusBefore.onclick = () => insertEvent(itinIdx, dayIdx, evIndex, 'before'); p.appendChild(plusBefore);
          // [删] delete current
          const delBtn = document.createElement('button'); delBtn.className = 'mini-btn'; delBtn.innerText = '删'; delBtn.title = '删除此项'; delBtn.onclick = () => deleteEvent(itinIdx, dayIdx, evIndex); p.appendChild(delBtn);
          // [当前类别]
          const curKey = (ev.type === 'drive') ? 'drive' : (ev.type === 'accommodation' ? 'accommodation' : (ev.subtype === 'food' ? 'food' : 'spot'));
          const curLabel = (curKey === 'drive' ? '驾驶' : curKey === 'accommodation' ? '住宿' : curKey === 'food' ? '美食' : '游玩');
          const typeBtn = document.createElement('button'); typeBtn.className = 'mini-btn'; typeBtn.innerText = curLabel; typeBtn.title = '切换类别';
          typeBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openCategoryMenu(typeBtn, itinIdx, dayIdx, evIndex, curKey); };
          p.appendChild(typeBtn);
          // [输入框]
          const input = document.createElement('input'); input.type = 'text'; input.value = String(ev.text || ev.title || ''); input.style.flex = '1'; input.style.minWidth = '120px';
          input.onfocus = () => { p.setAttribute('draggable', 'false'); };
          input.onblur = () => {
            p.setAttribute('draggable', 'true');
            const v = input.value.trim();
            const targetEv = editingDays[dayIdx].events[evIndex];
            targetEv.text = v;
            if (targetEv.type === 'drive') {
              const parts = v.split(/→|->/);
              const from = (parts[0] || '').trim(); const to = (parts[1] || '').trim();
              if (from && to) { targetEv.from = from; targetEv.to = to; targetEv.distance = null; targetEv.duration = null; targetEv.fromCoords = null; targetEv.toCoords = null; }
            }
          };
          input.oninput = () => { const targetEv = editingDays[dayIdx].events[evIndex]; targetEv.text = input.value; };
          p.appendChild(input);
        } else {
          // view-only row content
          let a;
          if (ev.type === 'drive') {
            a = document.createElement('a'); a.href = buildAmapNavUrl(ev.from, ev.fromCoords, ev.to, ev.toCoords); a.target = '_blank'; a.rel = 'noopener noreferrer'; 
            const distStr = ev.distance || '';
            const durStr = ev.duration || '';
            const metaStr = (distStr || durStr) ? `  （${distStr}${distStr && durStr ? '，' : ''}${durStr}）` : '';
            a.innerText = `驾驶：${ev.text}${metaStr}`;
            p.appendChild(a);
          } else if (ev.type === 'activity') {
            a = document.createElement('a'); a.target = '_blank'; a.rel = 'noopener noreferrer'; if (ev.subtype === 'food') { a.href = buildDianpingUrl(ev.text); a.innerText = `美食：${ev.text}`; } else { a.href = buildCtripUrl(ev.text); a.innerText = `游玩：${ev.text}`; } p.appendChild(a);
          } else if (ev.type === 'accommodation') {
            a = document.createElement('a'); a.href = buildCtripUrl(ev.text); a.target = '_blank'; a.rel = 'noopener noreferrer'; a.innerText = `住宿：${ev.text}`; p.appendChild(a);
          } else {
            const span = document.createElement('span'); span.textContent = String(ev && (ev.text || ev.title) || ''); p.appendChild(span);
          }
        }
        dayEl.appendChild(p);
      });
      // Show a visible "+ 添加" button under the last event (edit mode)
      if (isEditing && editingItineraryIndex === itinIdx) {
        const addWrap = document.createElement('div');
        addWrap.style.display = 'flex';
        addWrap.style.margin = '6px 0 4px';
        const addBtn = document.createElement('button');
        addBtn.className = 'mini-btn';
        addBtn.textContent = '+ 添加';
        addBtn.title = '在当天末尾新增一项';
        addBtn.onclick = () => addEventAtEnd(itinIdx, dayIdx);
        addWrap.appendChild(addBtn);
        dayEl.appendChild(addWrap);
      }
      // If only accommodation field exists without an explicit event, render it once
      try {
        const hasAccEv = (d.events || []).some(ev => ev && ev.type === 'accommodation');
        if (d.accommodation && !hasAccEv) {
          const p = document.createElement('div'); p.className = 'event-row';
          const a = document.createElement('a'); a.href = buildCtripUrl(String(d.accommodation)); a.target = '_blank'; a.rel = 'noopener noreferrer'; a.innerText = `住宿：${String(d.accommodation)}`;
          if (isEditing && editingItineraryIndex === itinIdx) { a.addEventListener('click', e => e.preventDefault()); a.style.pointerEvents = 'none'; a.style.color = '#333'; }
          p.appendChild(a); dayEl.appendChild(p);
        }
      } catch { }
      // After listing events, add a bottom drop zone for appending at end (editing mode)
      if (isEditing && editingItineraryIndex === itinIdx) {
        const bottomZone = document.createElement('div');
        bottomZone.className = 'day-dropzone';
        bottomZone.dataset.dayIndex = String(dayIdx);
        bottomZone.dataset.pos = 'bottom';
        bottomZone.dataset.itinIndex = String(itinIdx);
        bottomZone.addEventListener('dragover', (e) => { if (dragState) { e.preventDefault(); bottomZone.classList.add('active'); } });
        bottomZone.addEventListener('dragleave', () => bottomZone.classList.remove('active'));
        bottomZone.addEventListener('drop', (e) => { e.preventDefault(); bottomZone.classList.remove('active'); if (!dragState) return; const toIndex = (d.events || []).length; moveEvent(dragState.itinIdx, dragState.dayIdx, dragState.fromIndex, itinIdx, dayIdx, toIndex); dragState = null; });
        setupTouchZone(bottomZone, itinIdx, dayIdx, 'bottom');
        dayEl.appendChild(bottomZone);
      }
      dayEl.style.marginBottom = '12px'; daysList.appendChild(dayEl);
    });
    wrap.appendChild(daysList); itineraryList.appendChild(wrap);
  });
}

async function insertEvent(itinIdx, dayIdx, evIdx, where) { if (!isEditing || !editingDays || editingItineraryIndex !== itinIdx) return; const ev = await promptNewEvent(); if (!ev) return; const arr = editingDays[dayIdx].events; const insertIndex = where === 'before' ? evIdx : evIdx + 1; arr.splice(insertIndex, 0, ev); renderItineraryOverview(); }
async function promptNewEvent() { const type = (await showPrompt('输入类型：drive / food / spot / accommodation', 'spot') || '').trim().toLowerCase(); if (!type) return null; if (type === 'drive') { const raw = await showPrompt('输入驾驶段：格式  出发地 → 目的地', '杭州 → 黄山'); if (!raw) return null; const parts = raw.split(/→|->/); const from = (parts[0] || '').trim(); const to = (parts[1] || '').trim(); if (!from || !to) return null; return { type: 'drive', text: `${from} → ${to}`, from, to, distance: null, duration: null }; } else if (type === 'food') { const name = await showPrompt('输入美食名称', '徽州菜馆'); if (!name) return null; return { type: 'activity', text: name.trim(), subtype: 'food' }; } else if (type === 'accommodation') { const name = await showPrompt('输入住宿名称', '宏村古宅客栈'); if (!name) return null; return { type: 'accommodation', text: name.trim() }; } else { const name = await showPrompt('输入游玩项目名称', '黄山'); if (!name) return null; return { type: 'activity', text: name.trim(), subtype: 'spot' }; } }
async function editEvent(itinIdx, dayIdx, evIdx) { if (!isEditing || !editingDays || editingItineraryIndex !== itinIdx) return; const ev = editingDays[dayIdx].events[evIdx]; if (!ev) return; if (ev.type === 'drive') { const raw = await showPrompt('修改驾驶段：格式  出发地 → 目的地', ev.text || ''); if (!raw) return; const parts = raw.split(/→|->/); const from = (parts[0] || '').trim(); const to = (parts[1] || '').trim(); if (!from || !to) return; ev.text = `${from} → ${to}`; ev.from = from; ev.to = to; ev.distance = null; ev.duration = null; } else if (ev.type === 'activity') { const name = await showPrompt('修改名称', ev.text || ''); if (!name) return; let subtype = ev.subtype || 'spot'; const kind = (await showPrompt('类型：food / spot', subtype)) || subtype; ev.text = name.trim(); ev.subtype = (kind.trim().toLowerCase() === 'food') ? 'food' : 'spot'; } else if (ev.type === 'accommodation') { const name = await showPrompt('修改住宿名称', ev.text || ''); if (!name) return; ev.text = name.trim(); } renderItineraryOverview(); }
async function deleteEvent(itinIdx, dayIdx, evIdx) { if (!isEditing || !editingDays || editingItineraryIndex !== itinIdx) return; const ok = await showConfirm('确认删除该项？'); if (!ok) return; const arr = editingDays[dayIdx].events; arr.splice(evIdx, 1); if (arr.length === 0) { editingDays.splice(dayIdx, 1); } renderItineraryOverview(); }
function moveEvent(fromItin, fromDay, fromIndex, toItin, toDay, toIndex) { if (!isEditing || !editingDays || editingItineraryIndex !== toItin || fromItin !== toItin) return; if (fromDay < 0 || toDay < 0) return; if (fromDay >= editingDays.length || toDay >= editingDays.length) return; const fromArr = editingDays[fromDay].events; const item = fromArr && fromArr[fromIndex]; if (!fromArr || !item) return; fromArr.splice(fromIndex, 1); let removedDay = false; if (fromArr.length === 0) { editingDays.splice(fromDay, 1); removedDay = true; } let targetDay = toDay; if (removedDay && fromDay < toDay) { targetDay = toDay - 1; } if (targetDay < 0 || targetDay >= editingDays.length) { return renderItineraryOverview(); } const destArr = editingDays[targetDay].events; let adjIndex = toIndex; if (!removedDay && targetDay === fromDay && fromIndex < toIndex) { adjIndex = toIndex - 1; } const clampedIndex = Math.max(0, Math.min(adjIndex, destArr.length)); destArr.splice(clampedIndex, 0, item); renderItineraryOverview(); }

function setupTouchRow(rowEl, itinIdx, dayIdx, evIndex) {
  const onStart = (e) => { if (!isEditing) return; const t = e.touches && e.touches[0]; if (!t) return; touchDrag = { active: true, itinIdx, fromDay: dayIdx, fromIndex: evIndex, ghostEl: createGhost(rowEl, t.clientX, t.clientY), over: null }; document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('touchend', onEnd); document.addEventListener('touchcancel', onEnd); };
  const onMove = (e) => { if (!touchDrag || !touchDrag.active) return; const t = e.touches && e.touches[0]; if (!t) return; positionGhost(touchDrag.ghostEl, t.clientX, t.clientY); autoScrollDrawer(t.clientY); const el = document.elementFromPoint(t.clientX, t.clientY); clearTouchHighlights(); const row = el && el.closest && el.closest('.event-row'); const zone = el && el.closest && el.closest('.day-dropzone'); if (row) { row.classList.add('drag-over'); const d = parseInt(row.dataset.dayIndex || '-1', 10); const i = parseInt(row.dataset.evIndex || '-1', 10); const it = parseInt(row.dataset.itinIndex || '-1', 10); touchDrag.over = { type: 'row', itin: it, day: d, index: i }; } else if (zone) { zone.classList.add('active'); const d = parseInt(zone.dataset.dayIndex || '-1', 10); const it = parseInt(zone.dataset.itinIndex || '-1', 10); const p = zone.dataset.pos === 'bottom' ? 'bottom' : 'top'; touchDrag.over = { type: 'zone', itin: it, day: d, pos: p }; } else { touchDrag.over = null; } e.preventDefault(); };
  const onEnd = () => { if (!touchDrag) return cleanup(); const over = touchDrag.over; const fromDay = touchDrag.fromDay; const fromIndex = touchDrag.fromIndex; const fromItin = touchDrag.itinIdx; cleanup(); if (!over) return; if (over.type === 'row') { moveEvent(fromItin, fromDay, fromIndex, over.itin, over.day, over.index); } else if (over.type === 'zone') { if (fromItin !== over.itin) return; const toIdx = (editingDays[over.day].events || []).length; moveEvent(fromItin, fromDay, fromIndex, over.itin, over.day, over.pos === 'top' ? 0 : toIdx); } };
  const cleanup = () => { clearTouchHighlights(); if (touchDrag && touchDrag.ghostEl) { touchDrag.ghostEl.remove(); } touchDrag = null; document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onEnd); document.removeEventListener('touchcancel', onEnd); };
  rowEl.addEventListener('touchstart', onStart);
}
function setupTouchZone(zoneEl, itinIdx, dayIdx, pos) { zoneEl.dataset.itinIndex = String(itinIdx); zoneEl.dataset.dayIndex = String(dayIdx); zoneEl.dataset.pos = pos; }
function createGhost(sourceEl, x, y) { const g = document.createElement('div'); g.className = 'touch-ghost'; g.textContent = sourceEl.textContent.trim(); document.body.appendChild(g); positionGhost(g, x, y); return g; }
function positionGhost(g, x, y) { const offsetX = 12, offsetY = 12; g.style.left = `${Math.max(4, x + offsetX)}px`; g.style.top = `${Math.max(4, y + offsetY)}px`; }
function clearTouchHighlights() { document.querySelectorAll('.event-row.drag-over').forEach(el => el.classList.remove('drag-over')); document.querySelectorAll('.day-dropzone.active').forEach(el => el.classList.remove('active')); }
function autoScrollDrawer(clientY) { const drawer = document.getElementById('itineraryDrawer'); const rect = drawer.getBoundingClientRect(); const threshold = 60; const maxStep = 16; if (clientY < rect.top + threshold) { drawer.scrollTop -= maxStep; } else if (clientY > rect.bottom - threshold) { drawer.scrollTop += maxStep; } }

async function loadItineraries() {
  try {
    itinerariesLoading = true; itinerariesError = null; renderItineraryOverview();
    itineraries = await api.getItineraries();
  } catch (e) {
    console.warn('loadItineraries failed', e);
    itinerariesError = e || true;
  } finally {
    itinerariesLoading = false; renderItineraryOverview();
  }
}
function persistItineraries() { api.setItineraries(itineraries); }

async function planTripWithAI() {
  try {
    initMapIfNeeded();
    const text = (tripInput && tripInput.value || '').trim();
    if (!text) { await showAlert('请输入行程需求'); return; }
    planningInProgress = true;
    if (tripInput) { tripInput.setAttribute('aria-busy', 'true'); tripInput.disabled = true; }
    if (planningSpinner) planningSpinner.style.display = 'inline-block';
    // Try backend AI generate first
    let aiItin = null;
    try {
      aiItin = await api.aiGenerate({ query: text });
    } catch (_) { aiItin = null; }
    if (aiItin && aiItin.days) {
      // Coerce shape and normalize empty days
      const rawDays = Array.isArray(aiItin.days) ? aiItin.days : [];
      let normDays = rawDays.map((d) => {
        const evs = Array.isArray(d?.events) ? d.events : [];
        // Some models may return separate arrays: places/spots/foods/hotels/drives
        const places = Array.isArray(d?.places) ? d.places : Array.isArray(d?.spots) ? d.spots : [];
        const foods = Array.isArray(d?.foods) ? d.foods : [];
        const hotels = Array.isArray(d?.hotels) ? d.hotels : [];
        const drives = Array.isArray(d?.drives) ? d.drives : [];
        let events = evs.slice();
        if (places.length) {
          places.forEach(p => events.push({ type: 'activity', text: (p.name || p.title || p).toString(), subtype: 'spot' }));
        }
        if (foods.length) {
          foods.forEach(f => events.push({ type: 'activity', text: (f.name || f.title || f).toString(), subtype: 'food' }));
        }
        if (drives.length) {
          drives.forEach(r => {
            const from = (r.from || r.start || r.src || '').toString();
            const to = (r.to || r.end || r.dst || '').toString();
            const text = (r.text || `${from} → ${to}`).toString();
            const distance = r.distance || null;
            const duration = r.duration || null;
            const fromCoords = Array.isArray(r.fromCoords) ? r.fromCoords : null;
            const toCoords = Array.isArray(r.toCoords) ? r.toCoords : null;
            if (from && to) {
              events.push({ type: 'drive', text, from, to, distance, duration, fromCoords, toCoords });
            }
          });
        }
        if (hotels.length) {
          hotels.forEach(h => events.push({ type: 'accommodation', text: (h.name || h.title || h).toString() }));
        }
        return { events: Array.isArray(events) ? events : [], accommodation: d?.accommodation || null };
      });
      // If still effectively empty, try to synthesize from a simple route inferred from the query
      const hasAny = normDays.some(d => (d.events || []).length > 0);
      if (!hasAny) {
        const text = (tripInput && tripInput.value || '').trim();
        const routeNames = parseTextToRoute(text);
        const coordsArray = routeNames.map(n => [POI_DB[n]?.lat, POI_DB[n]?.lng]).filter(x => Array.isArray(x) && x.every(n => typeof n === 'number'));
        if (routeNames.length >= 2 && coordsArray.length === routeNames.length) {
          normDays = buildItineraryOverview(routeNames, coordsArray, []);
        }
      }
      const itin = { id: aiItin.id || Date.now(), name: aiItin.name || '行程', createdAt: aiItin.createdAt || Date.now(), days: normDays };
      itineraries.push(itin);
      selectedItineraryIndex = itineraries.length - 1;
      isEditing = false; editingItineraryIndex = -1; editingDays = null;
      persistItineraries();
      renderItineraryOverview();
      updateMapFromItinerary(itin.days);
      await recomputeDriveMetaFor(selectedItineraryIndex);
      if (chatPage && chatPage.classList.contains('open')) renderChat();
      return;
    }
    // Fallback: local heuristic generator
    planTrip();
  } finally {
    if (planningSpinner) planningSpinner.style.display = 'none';
    planningInProgress = false;
    if (tripInput) { tripInput.removeAttribute('aria-busy'); tripInput.disabled = false; }
  }
}

async function updateMapFromItinerary(days) {
  try {
    if (!map) { return; }
    if (mapProvider === 'amap' && window.AMap && amapMap) {
      return await updateMapFromItineraryAmap(days);
    }
    if (!window.L) { return; }
    const daysArr = days || [];
    // Build coordinates path preferring backend-provided event coords; fallback to POI_DB by name
    const coordsArray = [];
    const usedNames = [];
    const sameCoord = (a, b) => Array.isArray(a) && Array.isArray(b) && Math.abs(a[0] - b[0]) < 1e-6 && Math.abs(a[1] - b[1]) < 1e-6;
    // Try extracting from drive events' coordinates
    daysArr.forEach(d => {
      (d.events || []).forEach(ev => {
        if (ev.type !== 'drive') return;
        let a = (Array.isArray(ev.fromCoords) && ev.fromCoords.length === 2) ? ev.fromCoords : null;
        let b = (Array.isArray(ev.toCoords) && ev.toCoords.length === 2) ? ev.toCoords : null;
        if (!a || !b) {
          const fp = POI_DB[ev.from || ''];
          const tp = POI_DB[ev.to || ''];
          if (fp && tp) { a = [fp.lat, fp.lng]; b = [tp.lat, tp.lng]; usedNames.push(ev.from, ev.to); }
        } else {
          usedNames.push(ev.from, ev.to);
        }
        if (a && b) {
          if (coordsArray.length === 0) { coordsArray.push(a); }
          // If the last coordinate differs from the segment start, insert it to keep continuity
          const last = coordsArray[coordsArray.length - 1];
          if (!sameCoord(last, a)) { coordsArray.push(a); }
          coordsArray.push(b);
        }
      });
    });
    // Deduplicate consecutive used names
    let routeNames = usedNames.filter((v, i, arr) => i === 0 || v !== arr[i - 1]).filter(Boolean);
    // Fallback: if no drive segments, try to synthesize path from activities/accommodations
    if (coordsArray.length < 2) {
      const altNames = [];
      const altCoords = [];
      daysArr.forEach(d => {
        (d.events || []).forEach(ev => {
          if (ev.type === 'activity' || ev.type === 'accommodation') {
            const name = String(ev.text || ev.name || '').trim();
            if (!name) return;
            let pt = null;
            if (Array.isArray(ev.coords) && ev.coords.length === 2) {
              pt = ev.coords;
            } else {
              const p = POI_DB[name];
              if (p) pt = [p.lat, p.lng];
            }
            if (pt) {
              // push name and coord if not same as last
              if (altCoords.length === 0 || !sameCoord(altCoords[altCoords.length - 1], pt)) {
                altNames.push(name);
                altCoords.push(pt);
              }
            }
          }
        });
      });
      if (altCoords.length >= 2) {
        coordsArray.splice(0, coordsArray.length, ...altCoords);
        routeNames = altNames;
      }
    }
    if (markerLayer) markerLayer.clearLayers(); if (routeLayer) routeLayer.clearLayers();
    // Also add markers for activities and accommodations (non-drive) when coordinates can be resolved (always show these)
    const iconUrlFor = (kind) => kind === 'hotel' ? 'https://cdn-icons-png.flaticon.com/512/139/139899.png' : (kind === 'food' ? 'https://cdn-icons-png.flaticon.com/512/1046/1046784.png' : 'https://cdn-icons-png.flaticon.com/512/854/854878.png');
    daysArr.forEach(d => {
      (d.events || []).forEach(ev => {
        if (ev.type !== 'activity' && ev.type !== 'accommodation') return;
        const name = String(ev.text || ev.name || '').trim();
        if (!name) return;
        let pt = null;
        if (Array.isArray(ev.coords) && ev.coords.length === 2) pt = ev.coords;
        else {
          const p = POI_DB[name]; if (p) pt = [p.lat, p.lng];
        }
        if (!pt) return;
        const kind = (ev.type === 'accommodation') ? 'hotel' : (ev.subtype === 'food' ? 'food' : 'spot');
        const icon = window.L.icon({ iconUrl: iconUrlFor(kind), iconSize: [26, 26] });
        const hotelBtn = (kind === 'hotel') ? `<br><button style='margin-top:4px;padding:4px 8px;font-size:12px;border:1px solid #1a73e8;background:#1a73e8;color:#fff;border-radius:4px;cursor:pointer' onclick="window.open('${buildCtripUrl(name)}','_blank','noopener')">携程预订</button>` : '';
        const spotBtn = (kind === 'spot') ? `<br><button style='margin-top:4px;padding:4px 8px;font-size:12px;border:1px solid #ff7e1a;background:#ff7e1a;color:#fff;border-radius:4px;cursor:pointer' onclick="window.open('${buildCtripUrl(name)}','_blank','noopener')">携程订票</button>` : '';
        const foodBtn = (kind === 'food') ? `<br><button style='margin-top:4px;padding:4px 8px;font-size:12px;border:1px solid #21a366;background:#21a366;color:#fff;border-radius:4px;cursor:pointer' onclick="window.open('${buildDianpingUrl(name)}','_blank','noopener')">大众点评订餐</button>` : '';
        const navBtn = `<br><button style='margin-top:4px;padding:4px 8px;font-size:12px;border:1px solid #0aa858;background:#0aa858;color:#fff;border-radius:4px;cursor:pointer' onclick="openNavTo(${pt[0]},${pt[1]},'${name.replace(/'/g, "\\'")}')">导航</button>`;
        window.L.marker(pt, { icon }).addTo(markerLayer).bindPopup(`<div class=\"poi-popup\"><b>${name}</b>${hotelBtn}${spotBtn}${foodBtn}${navBtn}</div>`);
      });
    });
    // If we don't have at least 2 waypoints, just fit to markers and hide route info
    if (coordsArray.length < 2) {
      try { routeInfo.style.display = 'none'; } catch { }
      try { if (markerLayer) { const bounds = markerLayer.getBounds && markerLayer.getBounds(); if (bounds && bounds.isValid && bounds.isValid()) map.fitBounds(bounds, { padding: [40, 40] }); } } catch { }
      return;
    }
    // Place markers for route waypoints using coordinates; label with names when available
    const waypoints = coordsArray.map((c, idx) => ({ coords: c, name: routeNames[idx] || `地点${idx + 1}` }));
    waypoints.forEach((wp, idx) => {
      const marker = window.L.marker(wp.coords).addTo(markerLayer);
      const isEndpoint = (idx === 0 || idx === waypoints.length - 1);
      const safeName = String(wp.name || '目的地');
      const ctripUrl = buildCtripUrl(safeName);
      const bookingBtn = isEndpoint ? '' : '<button style="padding:4px 8px;font-size:12px;border:1px solid #1a73e8;background:#1a73e8;color:#fff;border-radius:4px;cursor:pointer" onclick="window.open(\'' + ctripUrl + '\',\'_blank\',\'noopener\')">携程预订</button>';
      const navBtn = '<button style="padding:4px 8px;font-size:12px;border:1px solid #0aa858;background:#0aa858;color:#fff;border-radius:4px;cursor:pointer" onclick="openNavTo(' + wp.coords[0] + ',' + wp.coords[1] + ',\'' + safeName.replace(/'/g, "\\'") + '\')">导航</button>';
      const html = '<div class="poi-popup"><b>' + safeName + '</b><br><div style="display:flex;gap:6px;margin-top:4px;flex-wrap:wrap">' + bookingBtn + navBtn + '</div></div>';
      marker.bindPopup(html);
    });
    await drawDrivingRouteSequential(coordsArray);
  } catch (e) { console.warn('updateMapFromItinerary failed', e); }
}

// WGS84 -> GCJ-02 conversion helpers for AMap display
function _outOfChina(lat, lng) { return (lng < 72.004 || lng > 137.8347) || (lat < 0.8293 || lat > 55.8271); }
function _transformLat(x, y) { let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0; ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320.0 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0; return ret; }
function _transformLng(x, y) { let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0; ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0; return ret; }
function wgs84ToGcj02(lat, lng) { if (_outOfChina(lat, lng)) return [lat, lng]; const a = 6378245.0; const ee = 0.00669342162296594323; let dLat = _transformLat(lng - 105.0, lat - 35.0); let dLng = _transformLng(lng - 105.0, lat - 35.0); const radLat = lat / 180.0 * Math.PI; let magic = Math.sin(radLat); magic = 1 - ee * magic * magic; const sqrtMagic = Math.sqrt(magic); dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI); dLng = (dLng * 180.0) / (a / sqrtMagic * Math.cos(radLat) * Math.PI); const mgLat = lat + dLat; const mgLng = lng + dLng; return [mgLat, mgLng]; }
// Approximate inverse: GCJ-02 -> WGS84 (2 iterations are typically sufficient for UI rendering)
function gcj02ToWgs84(gcjLat, gcjLng) {
  if (_outOfChina(gcjLat, gcjLng)) return [gcjLat, gcjLng];
  let lat = gcjLat, lng = gcjLng;
  for (let i = 0; i < 2; i++) {
    const t = wgs84ToGcj02(lat, lng);
    const dLat = t[0] - gcjLat;
    const dLng = t[1] - gcjLng;
    lat -= dLat; lng -= dLng;
  }
  return [lat, lng];
}

async function updateMapFromItineraryAmap(days) {
  try {
    const AMap = window.AMap;
    // Clear previous overlays
    if (amapLocateCircle) { try { amapMap.remove(amapLocateCircle); } catch { } amapLocateCircle = null; }
    if (Array.isArray(amapMarkers) && amapMarkers.length) { try { amapMap.remove(amapMarkers); } catch { } amapMarkers = []; }
    if (Array.isArray(amapDrivingSegments) && amapDrivingSegments.length) { try { amapDrivingSegments.forEach(d => d && d.clear && d.clear()); } catch { } amapDrivingSegments = []; }
    const daysArr = days || [];
    // 1) Resolve missing coordinates from names using POI_DB or AMap.PlaceSearch
    const resolved = new Map(); // name -> [lat, lng] in WGS84
    const needResolve = new Set();
    const addNeed = (name) => { if (!name) return; const n = String(name).trim(); if (!n) return; if (POI_DB[n]) return; needResolve.add(n); };
    daysArr.forEach(d => {
      (d.events || []).forEach(ev => {
        if (ev.type === 'drive') { addNeed(ev.from); addNeed(ev.to); }
        else if (ev.type === 'activity' || ev.type === 'accommodation') {
          if (!Array.isArray(ev.coords) || ev.coords.length !== 2) addNeed(ev.text || ev.name);
        }
      });
    });
    if (needResolve.size > 0) {
      await new Promise((r) => AMap.plugin('AMap.PlaceSearch', r));
      const ps = new AMap.PlaceSearch({ pageSize: 1 });
      for (const name of needResolve) {
        // eslint-disable-next-line no-await-in-loop
        await new Promise((res) => {
          try {
            ps.search(name, (status, result) => {
              try {
                if (status === 'complete' && result && result.poiList && result.poiList.pois && result.poiList.pois.length) {
                  const poi = result.poiList.pois[0];
                  if (poi && poi.location) {
                    const gcjLat = poi.location.lat; const gcjLng = poi.location.lng;
                    const wgs = gcj02ToWgs84(gcjLat, gcjLng);
                    resolved.set(name, wgs);
                  }
                }
              } catch { }
              res();
            });
          } catch (_) { res(); }
        });
      }
    }
    const coordsArray = [];
    const usedNames = [];
    const sameCoord = (a, b) => Array.isArray(a) && Array.isArray(b) && Math.abs(a[0] - b[0]) < 1e-6 && Math.abs(a[1] - b[1]) < 1e-6;
    daysArr.forEach(d => {
      (d.events || []).forEach(ev => {
        if (ev.type !== 'drive') return;
        let a = (Array.isArray(ev.fromCoords) && ev.fromCoords.length === 2) ? ev.fromCoords : null;
        let b = (Array.isArray(ev.toCoords) && ev.toCoords.length === 2) ? ev.toCoords : null;
        if (!a || !b) {
          const fp = POI_DB[ev.from || ''];
          const tp = POI_DB[ev.to || ''];
          if (fp && tp) { a = [fp.lat, fp.lng]; b = [tp.lat, tp.lng]; usedNames.push(ev.from, ev.to); }
          else {
            const ra = resolved.get(String(ev.from || '').trim());
            const rb = resolved.get(String(ev.to || '').trim());
            if (ra && rb) { a = ra; b = rb; usedNames.push(ev.from, ev.to); }
          }
        } else { usedNames.push(ev.from, ev.to); }
        if (a && b) {
          if (coordsArray.length === 0) { coordsArray.push(a); }
          const last = coordsArray[coordsArray.length - 1];
          if (!sameCoord(last, a)) { coordsArray.push(a); }
          coordsArray.push(b);
        }
      });
    });
    let routeNames = usedNames.filter((v, i, arr) => i === 0 || v !== arr[i - 1]).filter(Boolean);
    // Fallback to activities/accommodations if no drive path found
    if (coordsArray.length < 2) {
      const alt = [];
      const names = [];
      daysArr.forEach(d => {
        (d.events || []).forEach(ev => {
          if (ev.type === 'activity' || ev.type === 'accommodation') {
            const nm = String(ev.text || ev.name || '').trim(); if (!nm) return;
            let pt = null;
            if (Array.isArray(ev.coords) && ev.coords.length === 2) pt = ev.coords;
            else { const p = POI_DB[nm]; if (p) pt = [p.lat, p.lng]; }
            if (pt) { if (!alt.length || !sameCoord(alt[alt.length - 1], pt)) { alt.push(pt); names.push(nm); } }
          }
        });
      });
      if (alt.length >= 2) { coordsArray.splice(0, coordsArray.length, ...alt); routeNames = names; }
    }
    // Add activity/accommodation markers (always add, even if no route)
    daysArr.forEach(d => {
      (d.events || []).forEach(ev => {
        if (ev.type !== 'activity' && ev.type !== 'accommodation') return;
        const name = String(ev.text || ev.name || '').trim(); if (!name) return;
        let pt = null;
        if (Array.isArray(ev.coords) && ev.coords.length === 2) pt = ev.coords;
        else {
          const p = POI_DB[name];
          if (p) pt = [p.lat, p.lng];
          else {
            const r = resolved.get(name);
            if (r) pt = r;
          }
        }
        if (!pt) return;
        const [lat, lng] = wgs84ToGcj02(pt[0], pt[1]);
        const m = new AMap.Marker({ position: [lng, lat], zIndex: 108 });
        const infowindow = new AMap.InfoWindow({ offset: new AMap.Pixel(0, -24) });
        const kind = (ev.type === 'accommodation') ? 'hotel' : (ev.subtype === 'food' ? 'food' : 'spot');
        const hotelBtn = (kind === 'hotel') ? `<br><button style='margin-top:4px;padding:4px 8px;font-size:12px;border:1px solid #1a73e8;background:#1a73e8;color:#fff;border-radius:4px;cursor:pointer' onclick="window.open('${buildCtripUrl(name)}','_blank','noopener')">携程预订</button>` : '';
        const spotBtn = (kind === 'spot') ? `<br><button style='margin-top:4px;padding:4px 8px;font-size:12px;border:1px solid #ff7e1a;background:#ff7e1a;color:#fff;border-radius:4px;cursor:pointer' onclick="window.open('${buildCtripUrl(name)}','_blank','noopener')">携程订票</button>` : '';
        const foodBtn = (kind === 'food') ? `<br><button style='margin-top:4px;padding:4px 8px;font-size:12px;border:1px solid #21a366;background:#21a366;color:#fff;border-radius:4px;cursor:pointer' onclick="window.open('${buildDianpingUrl(name)}','_blank','noopener')">大众点评订餐</button>` : '';
        const navBtn = `<br><button style='margin-top:4px;padding:4px 8px;font-size:12px;border:1px solid #0aa858;background:#0aa858;color:#fff;border-radius:4px;cursor:pointer' onclick="openNavTo(${pt[0]},${pt[1]},'${name.replace(/'/g, "\\'")}')">导航</button>`;
        const html = `<div class="poi-popup"><b>${name}</b>${hotelBtn}${spotBtn}${foodBtn}${navBtn}</div>`;
        m.on('click', () => { infowindow.setContent(html); infowindow.open(amapMap, m.getPosition()); });
        amapMarkers.push(m);
      });
    });
    if (amapMarkers.length) amapMap.add(amapMarkers);
    // If insufficient coordinates for routing, fit to markers and hide totals, then return
    if (coordsArray.length < 2) {
      try { routeInfo.style.display = 'none'; } catch { }
      try { amapMap.setFitView(amapMarkers); } catch { }
      return;
    }
    // Waypoint markers (for start/end and path points)
    const waypoints = coordsArray.map((c, idx) => ({ coords: c, name: routeNames[idx] || `地点${idx + 1}` }));
    const infowindow = new AMap.InfoWindow({ offset: new AMap.Pixel(0, -24) });
    waypoints.forEach((wp, idx) => {
      const [lat, lng] = wgs84ToGcj02(wp.coords[0], wp.coords[1]);
      const marker = new AMap.Marker({ position: [lng, lat], zIndex: 110 });
      const isEndpoint = (idx === 0 || idx === waypoints.length - 1);
      const safeName = String(wp.name || '目的地');
      const ctripUrl = buildCtripUrl(safeName);
      const bookingBtn = isEndpoint ? '' : `<button style="padding:4px 8px;font-size:12px;border:1px solid #1a73e8;background:#1a73e8;color:#fff;border-radius:4px;cursor:pointer" onclick="window.open('${ctripUrl}','_blank','noopener')">携程预订</button>`;
      const navBtn = `<button style=\"padding:4px 8px;font-size:12px;border:1px solid #0aa858;background:#0aa858;color:#fff;border-radius:4px;cursor:pointer\" onclick=\"openNavTo(${wp.coords[0]},${wp.coords[1]},'${safeName.replace(/'/g, "\\'")}')\">导航</button>`;
      const html = `<div class="poi-popup"><b>${safeName}</b><br><div style="display:flex;gap:6px;margin-top:4px;flex-wrap:wrap">${bookingBtn}${navBtn}</div></div>`;
      marker.on('click', () => { infowindow.setContent(html); infowindow.open(amapMap, marker.getPosition()); });
      amapMarkers.push(marker);
    });
    if (amapMarkers.length) amapMap.add(amapMarkers);
    // 不再默认显示固定的推荐POI，避免与用户行程无关的标注干扰
    try { amapMap.setFitView(amapMarkers); } catch { }
    // Routing using AMap.Driving (per segment)
    let totalDistance = 0; let totalDuration = 0;
    for (let i = 0; i < coordsArray.length - 1; i++) {
      const a = coordsArray[i]; const b = coordsArray[i + 1];
      const [aLat, aLng] = wgs84ToGcj02(a[0], a[1]);
      const [bLat, bLng] = wgs84ToGcj02(b[0], b[1]);
      const driving = new AMap.Driving({ map: amapMap, hideMarkers: true, showTraffic: false, policy: AMap.DrivingPolicy.LEAST_TIME });
      amapDrivingSegments.push(driving);
      await new Promise((resolve) => {
        driving.search(new AMap.LngLat(aLng, aLat), new AMap.LngLat(bLng, bLat), (status, result) => {
          if (status === 'complete' && result && result.routes && result.routes.length) {
            try { const r = result.routes[0]; totalDistance += (r.distance || 0); totalDuration += (r.time || r.duration || 0); } catch { }
          } else {
            try { const line = new AMap.Polyline({ path: [[aLng, aLat], [bLng, bLat]], strokeColor: '#999', strokeStyle: 'dashed', strokeWeight: 3 }); amapMap.add(line); } catch { }
          }
          resolve();
        });
      });
    }
    const km = (totalDistance / 1000).toFixed(1);
    const hours = (totalDuration / 3600).toFixed(2);
    routeInfo.style.display = 'block';
    routeInfo.innerHTML = `<div><b>总里程：</b>${km} km</div><div style="margin-top:6px"><b>预计驾驶：</b>${hours} 小时</div>`;
    try { amapMap.setFitView(); } catch { }
  } catch (e) { console.warn('updateMapFromItineraryAmap failed', e); }
}

async function drawDrivingRouteSequential(coordsArray) {
  if (!map || !window.L) return;
  if (routeLayer) { routeLayer.clearLayers(); }
  routeLayer = window.L.geoJSON(null, { style: { color: '#2b8cff', weight: 5, opacity: 0.9 } }).addTo(map);
  let totalDistance = 0; let totalDuration = 0;
  for (let i = 0; i < coordsArray.length - 1; i++) {
    const a = coordsArray[i]; const b = coordsArray[i + 1];
    const url = `https://router.project-osrm.org/route/v1/driving/${a[1]},${a[0]};${b[1]},${b[0]}?overview=full&geometries=geojson&steps=false`;
    try { const resp = await fetch(url); if (!resp.ok) throw new Error('route fetch failed'); const data = await resp.json(); if (data.code !== 'Ok' || !data.routes || data.routes.length === 0) throw new Error('no route'); const route = data.routes[0]; totalDistance += route.distance; totalDuration += route.duration; const geo = route.geometry; window.L.geoJSON(geo, { style: { color: '#2b8cff', weight: 5, opacity: 0.9 } }).addTo(routeLayer); } catch (err) { console.warn('路由请求失败，退回直线段：', err); window.L.polyline([a, b], { color: '#999', weight: 3, dashArray: '6 6' }).addTo(routeLayer); const meters = haversineMeters(a, b); totalDistance += meters; totalDuration += (meters / 1000) / 60 * 3600; }
  }
  const km = (totalDistance / 1000).toFixed(1); const hours = (totalDuration / 3600).toFixed(2);
  routeInfo.style.display = 'block'; routeInfo.innerHTML = `<div><b>总里程：</b>${km} km</div><div style="margin-top:6px"><b>预计驾驶：</b>${hours} 小时</div>`;
  try { map.fitBounds(routeLayer.getBounds(), { padding: [40, 40] }); } catch (e) { console.warn('fitBounds failed', e); }
}

// Edit-mode category popup helpers
let _catMenuEl = null;
function openCategoryMenu(anchorEl, itinIdx, dayIdx, evIndex, currentKey) {
  try { if (_catMenuEl) { _catMenuEl.remove(); _catMenuEl = null; } } catch { }
  const menu = document.createElement('div');
  // Basic menu styling and semantics
  Object.assign(menu.style, { position: 'absolute', zIndex: 2000, background: '#fff', border: '1px solid #ddd', borderRadius: '6px', boxShadow: '0 2px 12px rgba(0,0,0,0.15)', padding: '6px 0', minWidth: '140px' });
  menu.setAttribute('role', 'menu');
  const opts = [{ key: 'drive', label: '驾驶' }, { key: 'spot', label: '游玩' }, { key: 'food', label: '美食' }, { key: 'accommodation', label: '住宿' }, { key: 'cancel', label: '取消' }];
  opts.forEach(o => {
    const item = document.createElement('div'); item.textContent = (o.key === currentKey ? '● ' : '') + o.label; item.style.cssText = 'padding:6px 12px; cursor:pointer; white-space:nowrap; font-size:13px; outline:none;';
    item.setAttribute('role', 'menuitem');
    item.setAttribute('tabindex', '0');
    item.onmouseenter = () => item.style.background = '#f5f5f5';
    item.onmouseleave = () => item.style.background = 'transparent';
    const act = () => { if (o.key === 'cancel') { closeCategoryMenu(); return; } changeEventCategory(itinIdx, dayIdx, evIndex, o.key); closeCategoryMenu(); };
    item.onclick = (e) => { e.stopPropagation(); act(); };
    item.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); act(); } };
    menu.appendChild(item);
  });
  document.body.appendChild(menu); _catMenuEl = menu;
  const rect = anchorEl.getBoundingClientRect();
  // Viewport-aware positioning: open downward by default; if not enough space, open upward
  const vpH = window.innerHeight || document.documentElement.clientHeight || 800;
  const downTop = rect.bottom + 6 + window.scrollY;
  const upTop = Math.max(4, rect.top + window.scrollY - (menu.offsetHeight || 160) - 6);
  const openDown = (vpH - rect.bottom) >= (menu.offsetHeight || 160) + 8;
  menu.style.left = Math.max(4, rect.left + window.scrollX) + 'px';
  menu.style.top = (openDown ? downTop : upTop) + 'px';
  // Outside click and ESC to close
  const off = (ev) => { if (!_catMenuEl) return; if (!_catMenuEl.contains(ev.target)) closeCategoryMenu(); };
  const onEsc = (ev) => { if (ev.key === 'Escape') { closeCategoryMenu(); } };
  setTimeout(() => { document.addEventListener('mousedown', off, { once: true }); document.addEventListener('keydown', onEsc, { once: true }); }, 0);
}
function closeCategoryMenu() { if (_catMenuEl) { try { _catMenuEl.remove(); } catch { } _catMenuEl = null; } }
function changeEventCategory(itinIdx, dayIdx, evIndex, toKey) {
  if (!(isEditing && editingItineraryIndex === itinIdx && Array.isArray(editingDays))) return;
  const day = editingDays[dayIdx]; if (!day) return;
  const events = day.events || []; const ev = events[evIndex]; if (!ev) return;
  let newEv = null;
  if (toKey === 'drive') {
    const parts = String(ev.text || '').split(/→|->/).map(s => s.trim());
    const from = ev.from || parts[0] || ''; const to = ev.to || parts[1] || '';
    const text = (from && to) ? `${from} → ${to}` : (ev.text || '从A到B');
    newEv = { type: 'drive', text, from, to, subtype: 'car', distance: null, duration: null, fromCoords: null, toCoords: null };
  } else if (toKey === 'spot') {
    newEv = { type: 'activity', subtype: 'spot', text: ev.text || '景点' };
  } else if (toKey === 'food') {
    newEv = { type: 'activity', subtype: 'food', text: ev.text || '餐饮' };
  } else if (toKey === 'accommodation') {
    newEv = { type: 'accommodation', text: ev.text || '住宿' };
  } else { return; }
  events[evIndex] = newEv; day.events = events; renderItineraryOverview();
}

// Helper: fuzzy match location name in POI_DB
function findPOICoords(locationName) {
  if (!locationName) return null;
  const name = String(locationName).trim();
  // Exact match
  if (POI_DB[name]) return [POI_DB[name].lat, POI_DB[name].lng];
  // Fuzzy match: check if name contains or is contained by any POI_DB key
  for (const [key, value] of Object.entries(POI_DB)) {
    if (name.includes(key) || key.includes(name)) {
      return [value.lat, value.lng];
    }
  }
  return null;
}

async function recomputeDriveMetaFor(itinIdx) {
  try {
    const itin = itineraries[itinIdx];
    if (!itin) return;
    const days = itin.days || [];
    
    // Collect all location names that need geocoding
    const needGeocode = new Set();
    for (const d of days) {
      for (const ev of (d.events || [])) {
        if (ev.type !== 'drive') continue;
        // Check if we need to geocode the from/to locations
        let a = (Array.isArray(ev.fromCoords) && ev.fromCoords.length === 2) ? ev.fromCoords : null;
        let b = (Array.isArray(ev.toCoords) && ev.toCoords.length === 2) ? ev.toCoords : null;
        // Try fuzzy POI_DB lookup before marking for geocoding
        if (!a && ev.from) {
          const coords = findPOICoords(ev.from);
          if (!coords) needGeocode.add(ev.from);
        }
        if (!b && ev.to) {
          const coords = findPOICoords(ev.to);
          if (!coords) needGeocode.add(ev.to);
        }
      }
    }
    
    // Use AMap to geocode missing locations if available
    const geocoded = new Map();
    if (needGeocode.size > 0 && mapProvider === 'amap' && window.AMap && amapMap) {
      await new Promise((r) => window.AMap.plugin('AMap.PlaceSearch', r));
      const ps = new window.AMap.PlaceSearch({ pageSize: 1 });
      for (const name of needGeocode) {
        // eslint-disable-next-line no-await-in-loop
        await new Promise((res) => {
          try {
            ps.search(name, (status, result) => {
              try {
                if (status === 'complete' && result && result.poiList && result.poiList.pois && result.poiList.pois.length) {
                  const poi = result.poiList.pois[0];
                  if (poi && poi.location) {
                    const gcjLat = poi.location.lat; const gcjLng = poi.location.lng;
                    const wgs = gcj02ToWgs84(gcjLat, gcjLng);
                    geocoded.set(name, wgs);
                  }
                }
              } catch { }
              res();
            });
          } catch (_) { res(); }
        });
      }
    }
    
    for (const d of days) {
      for (const ev of (d.events || [])) {
        if (ev.type !== 'drive') continue;
        // If backend already provided complete info, don't override it
        if (ev.distance && ev.duration && Array.isArray(ev.fromCoords) && Array.isArray(ev.toCoords)) {
          continue;
        }
        // Prefer coordinates provided by backend; otherwise try local POI lookup
        let a = (Array.isArray(ev.fromCoords) && ev.fromCoords.length === 2) ? ev.fromCoords : null;
        let b = (Array.isArray(ev.toCoords) && ev.toCoords.length === 2) ? ev.toCoords : null;
        if (!a || !b) {
          // Try fuzzy POI_DB lookup first
          if (!a) a = findPOICoords(ev.from);
          if (!b) b = findPOICoords(ev.to);
          
          // If still not found, try geocoded results
          if (!a && ev.from) a = geocoded.get(ev.from) || null;
          if (!b && ev.to) b = geocoded.get(ev.to) || null;
        }
        // If we still can't determine coords, keep existing values (don't null them out)
        if (!a || !b) {
          continue;
        }
        const url = `https://router.project-osrm.org/route/v1/driving/${a[1]},${a[0]};${b[1]},${b[0]}?overview=false&geometries=geojson&steps=false`;
        try {
          const resp = await fetch(url);
          if (!resp.ok) throw new Error('route fetch failed');
          const data = await resp.json();
          if (data.code !== 'Ok' || !data.routes || data.routes.length === 0) throw new Error('no route');
          const route = data.routes[0];
          ev.distance = `${(route.distance / 1000).toFixed(1)} km`;
          ev.duration = `${(route.duration / 3600).toFixed(2)} h`;
          ev.fromCoords = a;
          ev.toCoords = b;
        } catch (e) {
          // Fallback: straight-line estimate at 60km/h
          const meters = haversineMeters(a, b);
          const km = meters / 1000;
          const h = km / 60;
          ev.distance = `${km.toFixed(1)} km`;
          ev.duration = `${h.toFixed(2)} h`;
          ev.fromCoords = a;
          ev.toCoords = b;
        }
      }
    }
    await api.setItineraries(itineraries);
    renderItineraryOverview();
  } catch (e) {
    console.warn('recomputeDriveMetaFor failed', e);
  }
}

function planTrip() {
  initMapIfNeeded();
  const text = tripInput.value.trim(); if (!text) { alert('请输入行程需求'); return; }
  // Clear layers if using Leaflet
  if (markerLayer) markerLayer.clearLayers(); 
  if (routeLayer) routeLayer.clearLayers(); 
  // Clear AMap markers if using AMap
  if (mapProvider === 'amap' && window.AMap && amapMap) {
    try { 
      if (amapMarkers.length) amapMap.remove(amapMarkers); 
      amapMarkers = []; 
      amapDrivingSegments.forEach(d => { try { d.clear(); } catch (_) { } }); 
      amapDrivingSegments = []; 
    } catch (_) { }
  }
  itineraryList.innerHTML = '';
  const routeNames = parseTextToRoute(text); 
  const coordsArray = routeNames.map(n => [POI_DB[n].lat, POI_DB[n].lng]);
  const days = buildItineraryOverview(routeNames, coordsArray, []);
  const name = `${routeNames[0]}→${routeNames[routeNames.length - 1]} 行程`;
  const itin = { id: Date.now(), name, createdAt: Date.now(), days };
  itineraries.push(itin); selectedItineraryIndex = itineraries.length - 1; isEditing = false; editingItineraryIndex = -1; editingDays = null; persistItineraries(); renderItineraryOverview();
  // Use updateMapFromItinerary to support both AMap and Leaflet providers
  // This will draw routes, markers, and show the total distance/time card
  updateMapFromItinerary(days);
  recomputeDriveMetaFor(selectedItineraryIndex);
}
window.planTrip = planTrip; // used by Enter key inline handler

// Append a new event at the end of a day (edit mode)
function addEventAtEnd(itinIdx, dayIdx) {
  if (!(isEditing && editingItineraryIndex === itinIdx && Array.isArray(editingDays))) return;
  const ev = promptNewEvent();
  if (!ev) return;
  const day = editingDays[dayIdx];
  if (!day) return;
  day.events = Array.isArray(day.events) ? day.events : [];
  day.events.push(ev);
  renderItineraryOverview();
}

// Compliance menu: simple feedback prompt
if (complianceFeedbackLink) {
  complianceFeedbackLink.addEventListener('click', async (e) => {
    e.preventDefault();
    try {
      const txt = (prompt('反馈/建议：') || '').trim();
      if (!txt) return;
      await api.submitFeedback({ text: txt, meta: { page: location.pathname } });
      alert('感谢反馈，我们会尽快查看。');
    } catch (_) { alert('提交失败，请稍后重试'); }
  });
}

// Share from itinerary drawer
let pendingShareItinIdx = null;
const itinShareOverlay = document.getElementById('itinShareOverlay');
const itinShareToCompanionBtn = document.getElementById('itinShareToCompanionBtn');
const itinShareToGroupBtn = document.getElementById('itinShareToGroupBtn');
const itinShareToWxBtn = document.getElementById('itinShareToWxBtn');
const itinShareCancelBtn = document.getElementById('itinShareCancelBtn');
const itinPickOverlay = document.getElementById('itinPickOverlay');
const itinPickTitle = document.getElementById('itinPickTitle');
const itinPickList = document.getElementById('itinPickList');
const itinPickCancelBtn = document.getElementById('itinPickCancelBtn');

function openItineraryShareSheet(itinIdx) { pendingShareItinIdx = itinIdx; itinShareOverlay.classList.add('open'); itinShareOverlay.setAttribute('aria-hidden', 'false'); }
function closeItineraryShareSheet() { itinShareOverlay.classList.remove('open'); itinShareOverlay.setAttribute('aria-hidden', 'true'); }
function openItinPick(kind) {
  itinPickList.innerHTML = '';
  if (kind === 'companion') {
    itinPickTitle.textContent = '选择同路人';
    // Mirror visibility rules from renderCompanions(): only self + entries I own
    const myId = currentUserId;
    const visible = (companions || []).filter(c => !!c && ((c.isSelf && (String(c.userId) === String(myId) || String(c.id) === `self-${myId}`)) || String(c.ownerUserId || '') === String(myId)));
    const seen = new Set();
    visible.forEach(c => {
      const uid = String(c.userId);
      if (seen.has(uid)) return; // dedupe by userId (reciprocal entries etc.)
      seen.add(uid);
      // Live name/contact refresh (user info may have changed)
      let liveName = c.name; let liveContact = c.contact;
      try { const u = (users || []).find(u => String(u.id) === uid); if (u) { liveName = u.nickname || u.phone || liveName; liveContact = u.phone || liveContact; } } catch { }
      const btn = document.createElement('button');
      btn.className = 'sheet-action';
      btn.textContent = liveName + (liveContact ? `（${liveContact}）` : '');
      btn.onclick = () => shareItineraryInternal('direct', c.id);
      itinPickList.appendChild(btn);
    });
    if (!itinPickList.children.length) { const empty = document.createElement('div'); empty.style.color = '#888'; empty.style.padding = '8px 0'; empty.textContent = '暂无同路人'; itinPickList.appendChild(empty); }
  } else {
    itinPickTitle.textContent = '选择同路人群';
    groups.forEach(g => { const btn = document.createElement('button'); btn.className = 'sheet-action'; btn.textContent = g.name; btn.onclick = () => shareItineraryInternal('group', g.id); itinPickList.appendChild(btn); });
    if (!itinPickList.children.length) { const empty = document.createElement('div'); empty.style.color = '#888'; empty.style.padding = '8px 0'; empty.textContent = '暂无群聊'; itinPickList.appendChild(empty); }
  }
  itinPickOverlay.classList.add('open'); itinPickOverlay.setAttribute('aria-hidden', 'false');
}
function closeItinPick() { itinPickOverlay.classList.remove('open'); itinPickOverlay.setAttribute('aria-hidden', 'true'); }
async function shareItineraryInternal(mode, targetId) { const itin = itineraries[pendingShareItinIdx]; if (!itin) return; const text = `[行程分享] ${itin.name}`; const snapshot = { name: itin.name, days: itin.days }; if (mode === 'group') { const arr = groupChats[targetId] = groupChats[targetId] || []; arr.push({ from: 'me', fromUserId: currentUserId, clientId, text, ts: Date.now(), type: 'share-itinerary', payload: { itinId: itin.id, snapshot } }); } else { const c = companions.find(x => String(x.id) === String(targetId)); const threadId = c ? dmThreadIdForUsers(currentUserId, c.userId || currentUserId) : String(targetId); const arr = chatSessions[threadId] = chatSessions[threadId] || []; arr.push({ from: 'me', fromUserId: currentUserId, clientId, text, ts: Date.now(), type: 'share-itinerary', payload: { itinId: itin.id, snapshot } }); } await persistChats(); closeItinPick(); closeItineraryShareSheet(); try { closePanel(); } catch { } if (mode === 'group') { openGroupChat(targetId); } else { openChat(targetId); } }
// Clipboard helper: try modern API; fall back to execCommand('copy')
async function tryCopyToClipboard(text) {
  // Modern API
  if (navigator.clipboard && navigator.clipboard.writeText) {
    try { await navigator.clipboard.writeText(text); return true; } catch (_) { /* fall through */ }
  }
  // Fallback: hidden textarea + execCommand
  try {
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.setAttribute('readonly', '');
    ta.style.position = 'fixed';
    ta.style.left = '-9999px';
    ta.style.top = '0';
    document.body.appendChild(ta);
    ta.select();
    ta.setSelectionRange(0, ta.value.length);
    const ok = document.execCommand && document.execCommand('copy');
    document.body.removeChild(ta);
    return !!ok;
  } catch (_) { return false; }
}

async function shareItineraryExternal() {
  const itin = itineraries[pendingShareItinIdx];
  if (!itin) return;
  const shareText = `我分享了一个行程：${itin.name}`;
  const url = (location.origin + location.pathname) + `#itin=${encodeURIComponent(itin.id)}`;
  const payload = `${shareText}\n${url}`;
  const onMobile = (typeof isMobile === 'function') ? isMobile() : /Android|webOS|iPhone|iPad|iPod/i.test(navigator.userAgent);
  if (onMobile && navigator.share) {
    try {
      await navigator.share({ title: itin.name, text: shareText, url });
    } catch (e) {
      const ok = await tryCopyToClipboard(payload);
      if (ok) { showMiniToast('已复制到剪贴板，可在微信粘贴分享'); } else { await showAlert('请手动复制链接:\n' + payload); }
    } finally { closeItineraryShareSheet(); }
    return;
  }
  // 桌面或不支持系统分享：直接复制并提示
  const ok = await tryCopyToClipboard(payload);
  if (ok) { showMiniToast('已复制到剪贴板，可在微信粘贴分享'); }
  else { await showAlert('请手动复制链接:\n' + payload); }
  closeItineraryShareSheet();
}

if (itinShareToCompanionBtn) { itinShareToCompanionBtn.addEventListener('click', () => { const user = users.find(u => u.id === currentUserId); if (!user) { postLoginRedirect = 'share-companion'; closeItineraryShareSheet(); if (profilePage) { profilePage.classList.add('open'); profilePage.setAttribute('aria-hidden', 'false'); if (profileBtn) profileBtn.setAttribute('aria-expanded', 'true'); renderLoginView(); pushAppState('profile'); } return; } closeItineraryShareSheet(); openItinPick('companion'); }); }
if (itinShareToGroupBtn) { itinShareToGroupBtn.addEventListener('click', () => { const user = users.find(u => u.id === currentUserId); if (!user) { postLoginRedirect = 'share-group'; closeItineraryShareSheet(); if (profilePage) { profilePage.classList.add('open'); profilePage.setAttribute('aria-hidden', 'false'); if (profileBtn) profileBtn.setAttribute('aria-expanded', 'true'); renderLoginView(); pushAppState('profile'); } return; } closeItineraryShareSheet(); openItinPick('group'); }); }
if (itinShareToWxBtn) itinShareToWxBtn.addEventListener('click', shareItineraryExternal);
if (itinShareCancelBtn) itinShareCancelBtn.addEventListener('click', closeItineraryShareSheet);
if (itinPickCancelBtn) itinPickCancelBtn.addEventListener('click', closeItinPick);
window.openItineraryShareSheet = openItineraryShareSheet;

// Click outside sheet to close (itinerary share)
if (itinShareOverlay) {
  itinShareOverlay.addEventListener('click', (e) => {
    const sheet = itinShareOverlay.querySelector('.sheet');
    if (sheet && !sheet.contains(e.target)) closeItineraryShareSheet();
  });
}
if (itinPickOverlay) {
  itinPickOverlay.addEventListener('click', (e) => {
    const sheet = itinPickOverlay.querySelector('.sheet');
    if (sheet && !sheet.contains(e.target)) closeItinPick();
  });
}
// Chat share overlays: tap blank (overlay) area to cancel
if (chatShareOverlay) {
  chatShareOverlay.addEventListener('click', (e) => {
    const sheet = chatShareOverlay.querySelector('.sheet');
    if (sheet && !sheet.contains(e.target)) closeShareSheet();
  });
}
if (chatShareItinOverlay) {
  chatShareItinOverlay.addEventListener('click', (e) => {
    const sheet = chatShareItinOverlay.querySelector('.sheet');
    if (sheet && !sheet.contains(e.target)) closeShareItinSheet();
  });
}

// Users and auth
let users = [];
let currentUserId = null;
let postLoginRedirect = null;
let loginPrefersStay = false; // when true, keep Profile open after login
async function loadUsers() { users = await api.getUsers(); currentUserId = await api.getCurrentUserId(); }
function persistUsers() { api.setUsers(users); }
function setCurrentUser(id) {
  currentUserId = id;
  api.setCurrentUserId(id);
  renderProfile();
  ensureSelfCompanion();
  // Manage invite polling lifecycle
  if (id) {
    startCompanionInvitePolling();
  } else {
    stopCompanionInvitePolling();
    companionInvites = [];
    lastCompanionInvitesSnapshot = [];
    updateCompanionsBadge();
  }
  // If companions page is visible, refresh the list
  try {
    if (companionsPage && companionsPage.classList.contains('open')) {
      renderCompanions();
    }
  } catch { }
  // If login initiated from Profile (我), keep Profile open
  if (id && loginPrefersStay) {
    loginPrefersStay = false;
    if (profilePage) { profilePage.classList.add('open'); profilePage.setAttribute('aria-hidden', 'false'); }
    if (profileBtn) profileBtn.setAttribute('aria-expanded', 'true');
    return;
  }
  if (id && postLoginRedirect) {
    const target = postLoginRedirect; postLoginRedirect = null;
    if (target === 'companions') {
      if (profilePage) { profilePage.classList.remove('open'); profilePage.setAttribute('aria-hidden', 'true'); if (profileBtn) profileBtn.setAttribute('aria-expanded', 'false'); }
      openCompanions();
    } else if (target === 'share-companion') {
      if (profilePage) { profilePage.classList.remove('open'); profilePage.setAttribute('aria-hidden', 'true'); if (profileBtn) profileBtn.setAttribute('aria-expanded', 'false'); }
      closeItineraryShareSheet(); openItinPick('companion');
    } else if (target === 'share-group') {
      if (profilePage) { profilePage.classList.remove('open'); profilePage.setAttribute('aria-hidden', 'true'); if (profileBtn) profileBtn.setAttribute('aria-expanded', 'false'); }
      closeItineraryShareSheet(); openItinPick('group');
    }
  }
  // Persist whatever is currently visible
  computeAndPersistActiveView();
}
function hashPwd(p) { return btoa(unescape(encodeURIComponent(p))).split('').reverse().join(''); }
function openProfile() {
  // Opening Profile explicitly cancels any stale redirect and prefers staying after login
  postLoginRedirect = null;
  loginPrefersStay = true;
  profilePage.classList.add('open');
  profilePage.setAttribute('aria-hidden', 'false');
  profileBtn.setAttribute('aria-expanded', 'true');
  renderProfile();
  markView('profile');
  pushAppState('profile');
}
function closeProfile() { profilePage.classList.remove('open'); profilePage.setAttribute('aria-hidden', 'true'); profileBtn.setAttribute('aria-expanded', 'false'); computeAndPersistActiveView(); }
if (profileBtn) profileBtn.addEventListener('click', openProfile);
if (closeProfileBtn) closeProfileBtn.addEventListener('click', closeProfile);

function renderProfile() {
  profileContent.innerHTML = ''; const user = users.find(u => u.id === currentUserId); if (!user) { renderLoginView(); return; } profileTitle.textContent = '我的账号'; const box = document.createElement('div'); box.style.padding = '8px 4px'; box.innerHTML = `<div style="font-weight:600;font-size:16px;margin-bottom:6px">昵称：${user.nickname}</div>
  <div style="color:#555;margin-bottom:12px">手机号：${user.phone}</div>
  <button id="logoutBtn" class="sheet-action" style="text-align:center">退出登录</button>`; profileContent.appendChild(box); const logoutBtn = box.querySelector('#logoutBtn'); if (logoutBtn) { logoutBtn.onclick = async () => { try { await api.logout(); } catch { } setCurrentUser(null); } }
}
function renderLoginView() {
  profileContent.innerHTML = ''; profileTitle.textContent = '登录'; const wrap = document.createElement('div'); wrap.innerHTML = `<div style="display:flex;flex-direction:column;gap:12px">
  <input id="loginPhone" placeholder="手机号" inputmode="numeric" autocomplete="tel" style="padding:10px 12px;border:1px solid #ddd;border-radius:8px" />
  <input id="loginPwd" type="password" placeholder="密码" autocomplete="current-password" style="padding:10px 12px;border:1px solid #ddd;border-radius:8px" />
  <div style="display:flex;gap:10px">
    <button id="doLoginBtn" class="companions-add-btn" style="flex:1">登录</button>
    <button id="goRegisterBtn" class="companions-close-btn" style="flex:1">注册</button>
  </div>
</div>`; profileContent.appendChild(wrap); wrap.querySelector('#doLoginBtn').onclick = handleLogin; wrap.querySelector('#goRegisterBtn').onclick = () => { renderRegisterStep1(); };
}
// override register button to real flow
try { Object.defineProperty(renderLoginView, '_patchedRegister', { value: true }); } catch { }
function validPhone(ph) { return /^1[3-9]\d{9}$/.test(ph); }
async function handleLogin() {
  const phone = (document.getElementById('loginPhone').value || '').trim();
  const pwd = (document.getElementById('loginPwd').value || '');
  if (!validPhone(phone)) { await showAlert('手机号格式不正确'); return; }
  try {
    const resp = await api.login({ phone, password: pwd });
    if (resp && resp.ok && resp.userId) {
      // Ensure local users cache has this user for profile rendering and companions
      try {
        const uid = resp.userId;
        const uobj = resp.user || null;
        const exists = users.some(u => String(u.id) === String(uid));
        if (!exists && uobj) { users.push({ id: uid, phone: uobj.phone || phone, nickname: uobj.nickname || (uobj.phone || '用户'), createdAt: Date.now() }); persistUsers(); }
      } catch { }
      setCurrentUser(resp.userId);
      return;
    }
    if (resp && resp.error) {
      if (resp.error === 'user_not_found') { await showAlert('用户不存在'); return; }
      if (resp.error === 'invalid_password') { await showAlert('密码错误'); return; }
    }
    // If resp is null or malformed, treat as service unavailable
    await showAlert('登录服务不可用，请稍后再试');
  } catch (_e) {
    await showAlert('登录服务不可用，请稍后再试');
  }
}
function renderRegisterStep1() {
  profileTitle.textContent = '注册 - 设置密码'; profileContent.innerHTML = ''; const wrap = document.createElement('div'); wrap.innerHTML = `<div style="display:flex;flex-direction:column;gap:12px">
  <input id="regPhone" placeholder="本人手机号（将作为登录唯一标识）" inputmode="numeric" autocomplete="tel" style="padding:10px 12px;border:1px solid #ddd;border-radius:8px" />
  <input id="regPwd1" type="password" placeholder="密码 (≥6位，勿与其它重要账号相同)" autocomplete="new-password" style="padding:10px 12px;border:1px solid #ddd;border-radius:8px" />
  <input id="regPwd2" type="password" placeholder="再次输入密码" autocomplete="new-password" style="padding:10px 12px;border:1px solid #ddd;border-radius:8px" />
  <label style="display:flex;align-items:flex-start;gap:6px;font-size:12px;line-height:1.4;color:#374151">
    <input id="regAgree" type="checkbox" style="margin-top:2px" />
    <span>我已阅读并同意 <a href="terms.html" target="_blank" rel="noopener">《用户协议》</a> 与 <a href="privacy.html" target="_blank" rel="noopener">《隐私政策》</a>，并承诺使用本人真实手机号注册；未满 18 周岁已征得监护人同意。</span>
  </label>
  <div style="font-size:11px;color:#6b7280;line-height:1.4">提示：注册表示您接受上述条款；如不同意，请停止注册。我们不会公开展示您的手机号。</div>
  <div style="display:flex;gap:10px;margin-top:4px">
    <button id="regNextBtn" class="companions-add-btn" style="flex:1">下一步</button>
    <button id="regCancelBtn" class="companions-close-btn" style="flex:1">取消</button>
  </div>
</div>`; profileContent.appendChild(wrap); wrap.querySelector('#regNextBtn').onclick = () => { const phone = (document.getElementById('regPhone').value || '').trim(); const p1 = document.getElementById('regPwd1').value || ''; const p2 = document.getElementById('regPwd2').value || ''; const agree = document.getElementById('regAgree').checked; if (!agree) { alert('请先勾选同意相关条款与声明'); return; } if (!validPhone(phone)) { alert('手机号格式不正确'); return; } if (p1.length < 6) { alert('密码至少6位'); return; } if (p1 !== p2) { alert('两次密码不一致'); return; } if (users.find(u => String(u.phone) === phone)) { alert('该手机号已注册'); return; } renderRegisterNickname({ phone, pwd: p1 }); }; wrap.querySelector('#regCancelBtn').onclick = renderLoginView;
}
function renderRegisterNickname(temp) {
  profileTitle.textContent = '注册 - 昵称'; profileContent.innerHTML = ''; const wrap = document.createElement('div'); wrap.innerHTML = `<div style="display:flex;flex-direction:column;gap:12px">
  <input id="regNickname" placeholder="输入昵称" style="padding:10px 12px;border:1px solid #ddd;border-radius:8px" />
  <div style="font-size:11px;color:#6b7280;line-height:1.4">继续完成注册即表示您再次确认已阅读并同意 <a href="terms.html" target="_blank" rel="noopener">《用户协议》</a> 与 <a href="privacy.html" target="_blank" rel="noopener">《隐私政策》</a>，并承诺使用本人真实手机号（未成年人已获监护人同意）。</div>
  <div style="display:flex;gap:10px">
    <button id="regFinishBtn" class="companions-add-btn" style="flex:1">完成注册</button>
    <button id="regBackBtn" class="companions-close-btn" style="flex:1">上一步</button>
  </div>
</div>`; profileContent.appendChild(wrap); wrap.querySelector('#regFinishBtn').onclick = async () => { const nick = (document.getElementById('regNickname').value || '').trim(); if (!nick) { alert('请输入昵称'); return; } let usedBackend = false; try { const resp = await api.register({ phone: temp.phone, password: temp.pwd, nickname: nick }); if (resp && resp.ok && resp.userId) { usedBackend = true; try { users = await api.getUsers(); } catch { } setCurrentUser(resp.userId); return; } if (resp && resp.error) { if (resp.error === 'phone_exists') { alert('该手机号已注册'); return; } if (resp.error === 'weak_password') { alert('密码太短'); return; } } } catch { } if (!usedBackend) { const id = Date.now(); users.push({ id, phone: temp.phone, passwordHash: hashPwd(temp.pwd), nickname: nick, createdAt: Date.now() }); persistUsers(); setCurrentUser(id); } }; wrap.querySelector('#regBackBtn').onclick = () => renderRegisterStep1();
}

// Map interactions and misc
// Map events are registered in initMapIfNeeded()

function showItineraryOnMap(itinIdx) {
  const itin = itineraries[itinIdx];
  if (!itin) return;
  selectedItineraryIndex = itinIdx;
  updateMapFromItinerary(itin.days);
  try { showMapDisclaimerOnce(); } catch (_) { }
}

function showMapDisclaimerOnce() {
  const id = 'mapDisclaimerToast';
  try { const old = document.getElementById(id); if (old) old.remove(); } catch { }
  const div = document.createElement('div');
  div.id = id;
  div.textContent = '标注的地点和路径仅供参考，可能存在偏差，请认真核对。';
  div.setAttribute('role', 'status');
  div.style.cssText = 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);max-width:82%;background:rgba(0,0,0,0.78);color:#fff;padding:10px 14px;font-size:12px;line-height:1.4;border-radius:8px;z-index:1600;box-shadow:0 4px 16px rgba(0,0,0,0.3);opacity:0;transition:opacity .25s';
  document.body.appendChild(div);
  requestAnimationFrame(() => { div.style.opacity = '1'; });
  setTimeout(() => { div.style.opacity = '0'; setTimeout(() => { try { div.remove(); } catch { } }, 320); }, 4500);
}

// Toast for third-party map service notice; siblings stack with nav/map disclaimers
function showMapServiceNoticeToast() {
  const id = 'mapServiceNoticeToast';
  try { const old = document.getElementById(id); if (old) old.remove(); } catch { }
  const div = document.createElement('div');
  div.id = id;
  div.textContent = '本网站使用第三方开源地图服务，地图可能加载失败，敬请谅解。';
  div.setAttribute('role', 'status');
  // Place slightly higher if other toasts present
  const baseBottom = 84; // to clear nav and map disclaimers (24 + 54 + padding)
  const offset = (document.getElementById('navDisclaimerToast') ? 30 : 0) + (document.getElementById('mapDisclaimerToast') ? 30 : 0);
  const bottomPx = baseBottom + offset;
  div.style.cssText = `position:fixed;left:50%;bottom:${bottomPx}px;transform:translateX(-50%);max-width:82%;background:rgba(0,0,0,0.78);color:#fff;padding:10px 14px;font-size:12px;line-height:1.4;border-radius:8px;z-index:1602;box-shadow:0 4px 16px rgba(0,0,0,0.3);opacity:0;transition:opacity .25s`;
  document.body.appendChild(div);
  requestAnimationFrame(() => { div.style.opacity = '1'; });
  setTimeout(() => { div.style.opacity = '0'; setTimeout(() => { try { div.remove(); } catch { } }, 320); }, 4800);
}

// Generic mini toast (stacking) for lightweight status updates
function showMiniToast(msg) {
  try {
    const contId = 'miniToastContainer';
    let cont = document.getElementById(contId);
    if (!cont) {
      cont = document.createElement('div');
      cont.id = contId;
      cont.style.cssText = 'position:fixed;left:50%;bottom:110px;transform:translateX(-50%);display:flex;flex-direction:column;gap:8px;z-index:1700;pointer-events:none;';
      document.body.appendChild(cont);
    }
    const div = document.createElement('div');
    div.textContent = msg;
    div.style.cssText = 'background:rgba(0,0,0,0.82);color:#fff;padding:8px 12px;border-radius:20px;font-size:12px;line-height:1.3;box-shadow:0 2px 8px rgba(0,0,0,0.25);opacity:0;transition:opacity .25s';
    cont.appendChild(div);
    requestAnimationFrame(() => { div.style.opacity = '1'; });
    setTimeout(() => { div.style.opacity = '0'; setTimeout(() => { try { div.remove(); if (!cont.children.length) cont.remove(); } catch { } }, 320); }, 3800);
  } catch { }
}
function clearMap() {
  try {
    if (mapProvider === 'amap' && amapMap) {
      try { if (amapLocateCircle) { amapMap.remove(amapLocateCircle); amapLocateCircle = null; } } catch { }
      try { if (amapMarkers && amapMarkers.length) { amapMap.remove(amapMarkers); amapMarkers = []; } } catch { }
      try { if (amapDrivingSegments && amapDrivingSegments.length) { amapDrivingSegments.forEach(d => d && d.clear && d.clear()); amapDrivingSegments = []; } } catch { }
    } else {
      if (markerLayer) markerLayer.clearLayers();
      if (routeLayer) routeLayer.clearLayers();
    }
    routeInfo.style.display = 'none';
  } catch (e) { }
}

// expose some functions used inline/in popups
window.openNavTo = openNavTo;
window.openItinPick = openItinPick;
window.closeItinPick = closeItinPick;
window.closeItineraryShareSheet = closeItineraryShareSheet;
window.closePanel = closePanel;
window.openPanel = openPanel;
window.togglePanel = togglePanel;
window.showItineraryOnMap = showItineraryOnMap;
